diff --git a/README.adoc b/README.adoc index 3f2cdff9..4b688e06 100644 --- a/README.adoc +++ b/README.adoc @@ -26,6 +26,8 @@ It also contains Vulkan usage clarifications, improved synchronization and new c The repository is organized into several important directories: * `en/` - Contains the tutorial content in English, organized by chapters +** The main tutorial covers fundamental Vulkan concepts (chapters 00-17) +** The "Building a Simple Engine" section builds upon these fundamentals to create a structured rendering engine * `attachments/` - Contains code examples, shader files, and resources used in the tutorial * `images/` - Contains illustrations, diagrams, and screenshots used in the tutorial * `scripts/` - Contains utility scripts, including dependency installation scripts diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 08155af5..7927901d 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -52,4 +52,13 @@ * xref:15_GLTF_KTX2_Migration.adoc[Migrating to Modern Asset Formats: glTF and KTX2] * xref:16_Multiple_Objects.adoc[Rendering Multiple Objects] * xref:17_Multithreading.adoc[Multithreading] +* xref:Building_a_Simple_Engine/introduction.adoc[Building a Simple Engine] +** xref:Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc[Engine Architecture] +** xref:Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc[Camera Transformations] +** xref:Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc[Lighting & Materials] +** xref:Building_a_Simple_Engine/GUI/01_introduction.adoc[GUI] +** xref:Building_a_Simple_Engine/Loading_Models/01_introduction.adoc[Loading Models] +** xref:Building_a_Simple_Engine/Subsystems/01_introduction.adoc[Subsystems] +** xref:Building_a_Simple_Engine/Tooling/01_introduction.adoc[Tooling] +** xref:Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc[Mobile Development] * xref:90_FAQ.adoc[FAQ] diff --git a/attachments/CMake/Findnlohmann_json.cmake b/attachments/CMake/Findnlohmann_json.cmake index 287c7e83..61dc66a6 100644 --- a/attachments/CMake/Findnlohmann_json.cmake +++ b/attachments/CMake/Findnlohmann_json.cmake @@ -47,7 +47,7 @@ if(NOT nlohmann_json_INCLUDE_DIR) FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git - GIT_TAG v3.11.2 # Use a specific tag for stability + GIT_TAG v3.12.0 # Use a specific tag for stability ) # Set policy to suppress the deprecation warning diff --git a/attachments/CMake/Findtinygltf.cmake b/attachments/CMake/Findtinygltf.cmake index 35d132c2..b2412350 100644 --- a/attachments/CMake/Findtinygltf.cmake +++ b/attachments/CMake/Findtinygltf.cmake @@ -16,11 +16,11 @@ find_package(nlohmann_json QUIET) if(NOT nlohmann_json_FOUND) include(FetchContent) - message(STATUS "nlohmann_json not found, fetching from GitHub...") + 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.11.2 # Use a specific tag for stability + GIT_TAG v3.12.0 # Use a specific tag for stability ) FetchContent_MakeAvailable(nlohmann_json) endif() diff --git a/attachments/simple_engine/Assets/grass-step-right.wav b/attachments/simple_engine/Assets/grass-step-right.wav new file mode 100644 index 00000000..7e1ee8ea Binary files /dev/null and b/attachments/simple_engine/Assets/grass-step-right.wav differ diff --git a/attachments/simple_engine/CMakeLists.txt b/attachments/simple_engine/CMakeLists.txt new file mode 100644 index 00000000..488b61cc --- /dev/null +++ b/attachments/simple_engine/CMakeLists.txt @@ -0,0 +1,208 @@ +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) + +# Add CMake module path for custom find modules +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../CMake") + +# Find required packages +find_package (glfw3 REQUIRED) +find_package (glm REQUIRED) +find_package (Vulkan REQUIRED) +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) + +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 +) + +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" +) + + +# Add the vulkan.cppm file directly as a source file +target_sources(VulkanCppModule + PRIVATE + "${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm" +) + +# Platform-specific settings +if(ANDROID) + # Android-specific settings + add_definitions(-DPLATFORM_ANDROID) +else() + # Desktop-specific settings + add_definitions(-DPLATFORM_DESKTOP) +endif() + +# Shader compilation +# Find Slang shaders +file(GLOB SLANG_SHADER_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.slang) + +# Find slangc executable (optional) +find_program(SLANGC_EXECUTABLE slangc HINTS $ENV{VULKAN_SDK}/bin) + +if(SLANGC_EXECUTABLE) + # Ensure the output directory for compiled shaders exists + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders) + + # Compile Slang shaders using slangc + foreach(SHADER ${SLANG_SHADER_SOURCES}) + get_filename_component(SHADER_NAME ${SHADER} NAME) + get_filename_component(SHADER_NAME_WE ${SHADER_NAME} NAME_WE) + string(REGEX REPLACE "\.slang$" "" OUTPUT_NAME ${SHADER_NAME}) + add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv + COMMAND ${SLANGC_EXECUTABLE} ${SHADER} -target spirv -profile spirv_1_4 -emit-spirv-directly -o ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv + DEPENDS ${SHADER} + COMMENT "Compiling Slang shader ${SHADER_NAME} with slangc" + ) + list(APPEND SHADER_SPVS ${CMAKE_CURRENT_BINARY_DIR}/shaders/${OUTPUT_NAME}.spv) + endforeach() + + add_custom_target(shaders DEPENDS ${SHADER_SPVS}) +else() + message(STATUS "slangc not found. Skipping shader compilation step.") + add_custom_target(shaders) +endif() + +# Source files +set(SOURCES + main.cpp + engine.cpp + scene_loading.cpp + platform.cpp + renderer_core.cpp + renderer_rendering.cpp + renderer_pipelines.cpp + renderer_compute.cpp + renderer_utils.cpp + renderer_resources.cpp + memory_pool.cpp + resource_manager.cpp + entity.cpp + component.cpp + transform_component.cpp + mesh_component.cpp + camera_component.cpp + model_loader.cpp + audio_system.cpp + physics_system.cpp + imgui_system.cpp + imgui/imgui.cpp + imgui/imgui_draw.cpp + vulkan_device.cpp + pipeline.cpp + descriptor_manager.cpp + renderdoc_debug_system.cpp +) + +# Create executable +add_executable(SimpleEngine ${SOURCES}) +add_dependencies(SimpleEngine shaders) +set_target_properties (SimpleEngine PROPERTIES CXX_STANDARD 20) + +# Link libraries +target_link_libraries(SimpleEngine PRIVATE + Vulkan::cppm + glm::glm + tinygltf::tinygltf + KTX::ktx + OpenAL::OpenAL +) + +if(NOT ANDROID) + target_link_libraries(SimpleEngine PRIVATE glfw) +endif() + +# Copy model and texture files if they exist +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/models) + add_custom_command(TARGET SimpleEngine POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/models ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/models + COMMENT "Copying models to output directory" + ) +endif() + +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/textures) + add_custom_command(TARGET SimpleEngine POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/textures ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/textures + COMMENT "Copying textures to output directory" + ) +endif() + +# Add packaging configuration +include(CPack) + +# Set package properties +set(CPACK_PACKAGE_NAME "SimpleEngine") +set(CPACK_PACKAGE_VENDOR "SimpleEngine Team") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A simple game engine built with Vulkan") +set(CPACK_PACKAGE_VERSION "1.0.0") +set(CPACK_PACKAGE_VERSION_MAJOR "1") +set(CPACK_PACKAGE_VERSION_MINOR "0") +set(CPACK_PACKAGE_VERSION_PATCH "0") +set(CPACK_PACKAGE_INSTALL_DIRECTORY "SimpleEngine") + +# Set platform-specific package generators +if(WIN32) + set(CPACK_GENERATOR "ZIP;NSIS") + set(CPACK_NSIS_PACKAGE_NAME "SimpleEngine") + set(CPACK_NSIS_DISPLAY_NAME "SimpleEngine") + set(CPACK_NSIS_HELP_LINK "https://github.com/yourusername/SimpleEngine") + set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/yourusername/SimpleEngine") + set(CPACK_NSIS_CONTACT "your.email@example.com") + set(CPACK_NSIS_MODIFY_PATH ON) +elseif(APPLE) + set(CPACK_GENERATOR "ZIP;DragNDrop") + set(CPACK_DMG_VOLUME_NAME "SimpleEngine") + set(CPACK_DMG_FORMAT "UDBZ") +else() + set(CPACK_GENERATOR "ZIP;DEB") + set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name ") + set(CPACK_DEBIAN_PACKAGE_SECTION "games") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libvulkan1, libglfw3, libglm-dev, libktx-dev") +endif() + +# Include binary and resource directories in the package +install(TARGETS SimpleEngine DESTINATION bin) +if(SLANGC_EXECUTABLE) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders DESTINATION share/SimpleEngine) +endif() + +# Install models and textures if they exist +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/models) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/models DESTINATION share/SimpleEngine) +endif() + +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/textures) + install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/textures DESTINATION share/SimpleEngine) +endif() + +# Install README if it exists +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/README.md) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/README.md DESTINATION share/SimpleEngine) +endif() diff --git a/attachments/simple_engine/audio_system.cpp b/attachments/simple_engine/audio_system.cpp new file mode 100644 index 00000000..673b03dd --- /dev/null +++ b/attachments/simple_engine/audio_system.cpp @@ -0,0 +1,1555 @@ +#include "audio_system.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// OpenAL headers +#ifdef __APPLE__ +#include +#include +#else +#include +#include +#endif + +#include "renderer.h" +#include "engine.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; + } +} + +// 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 +}; + +// 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; + } + } +}; + +AudioSystem::~AudioSystem() { + // Stop the audio thread first + stopAudioThread(); + + // Stop and clean up audio output device + if (outputDevice) { + outputDevice->Stop(); + outputDevice.reset(); + } + + // Destructor implementation + sources.clear(); + audioData.clear(); + + // 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; + } + } +} + +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 + } +} + +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::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); + + // 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)); + + 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::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; +} + +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::EnableHRTF(const bool enable) { + hrtfEnabled = enable; +} + +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; +} + +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::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; + } +} + +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 + } + + audioThreadShouldStop.store(false); + audioThreadRunning.store(true); + + audioThread = std::thread(&AudioSystem::audioThreadLoop, this); +} + +void AudioSystem::stopAudioThread() { + if (!audioThreadRunning.load()) { + return; // Thread not running + } + + // Signal the thread to stop + audioThreadShouldStop.store(true); + + // Wake up the thread if it's waiting + audioCondition.notify_all(); + + // Wait for the thread to finish + if (audioThread.joinable()) { + audioThread.join(); + } + + 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::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 +} + + + +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 new file mode 100644 index 00000000..4e5037c8 --- /dev/null +++ b/attachments/simple_engine/audio_system.h @@ -0,0 +1,402 @@ +#pragma once + +#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; +}; + +// Forward declarations +class Renderer; +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; +}; + +/** + * @brief Class for managing audio. + */ +class AudioSystem { +public: + /** + * @brief Default constructor. + */ + AudioSystem() = default; + + /** + * @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 new file mode 100644 index 00000000..45f1b9f5 --- /dev/null +++ b/attachments/simple_engine/camera_component.cpp @@ -0,0 +1,60 @@ +#include "camera_component.h" + +#include "entity.h" + +// Most of the CameraComponent class implementation is in the header file +// This file is mainly for any methods that might need additional implementation +// +// This implementation corresponds to the Camera_Transformations chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc + +// Initializes the camera by updating the view and projection matrices +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#camera-initialization +void CameraComponent::Initialize() { + UpdateViewMatrix(); + UpdateProjectionMatrix(); +} + +// Returns the view matrix, updating it if necessary +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#accessing-camera-matrices +const glm::mat4& CameraComponent::GetViewMatrix() { + if (viewMatrixDirty) { + UpdateViewMatrix(); + } + return viewMatrix; +} + +// Returns the projection matrix, updating it if necessary +// @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc#accessing-camera-matrices +const glm::mat4& CameraComponent::GetProjectionMatrix() { + if (projectionMatrixDirty) { + UpdateProjectionMatrix(); + } + return projectionMatrix; +} + +// Updates the view matrix based on the camera's position and orientation +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#view-matrix +void CameraComponent::UpdateViewMatrix() { + auto transformComponent = owner->GetComponent(); + if (transformComponent) { + 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; +} + +// 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; +} diff --git a/attachments/simple_engine/camera_component.h b/attachments/simple_engine/camera_component.h new file mode 100644 index 00000000..93968de1 --- /dev/null +++ b/attachments/simple_engine/camera_component.h @@ -0,0 +1,219 @@ +#pragma once + +#include +#include + +#include "component.h" +#include "transform_component.h" +#include "entity.h" + +/** + * @brief Component that handles the camera view and projection. + * + * This class implements the camera system as described in the Camera_Transformations chapter: + * @see en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc + */ +class CameraComponent : public Component { +public: + enum class ProjectionType { + Perspective, + Orthographic + }; + +private: + ProjectionType projectionType = ProjectionType::Perspective; + + // Perspective projection parameters + float fieldOfView = 45.0f; + float aspectRatio = 16.0f / 9.0f; + + // Orthographic projection parameters + float orthoWidth = 10.0f; + float orthoHeight = 10.0f; + + // Common parameters + float nearPlane = 0.1f; + float farPlane = 100.0f; + + // Matrices + glm::mat4 viewMatrix = glm::mat4(1.0f); + glm::mat4 projectionMatrix = glm::mat4(1.0f); + + // 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(); +}; diff --git a/attachments/simple_engine/component.cpp b/attachments/simple_engine/component.cpp new file mode 100644 index 00000000..f2cb04ba --- /dev/null +++ b/attachments/simple_engine/component.cpp @@ -0,0 +1,10 @@ +#include "component.h" + +// Most of the Component class implementation is in the header file +// This file is mainly for any methods that need to access the Entity class +// to avoid circular dependencies +// +// This implementation corresponds to the Engine_Architecture chapter in the tutorial: +// https://github.com/KhronosGroup/Vulkan-Tutorial/blob/master/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc + +// No additional implementation needed for now diff --git a/attachments/simple_engine/component.h b/attachments/simple_engine/component.h new file mode 100644 index 00000000..65480c71 --- /dev/null +++ b/attachments/simple_engine/component.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include + +// Forward declaration +class Entity; + +/** + * @brief Base class for all components in the engine. + * + * Components are the building blocks of the entity-component system. + * Each component encapsulates a specific behavior or property. + * + * 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; + +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 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 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 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 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; } +}; diff --git a/attachments/simple_engine/crash_reporter.h b/attachments/simple_engine/crash_reporter.h new file mode 100644 index 00000000..12c7ab4f --- /dev/null +++ b/attachments/simple_engine/crash_reporter.h @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "dbghelp.lib") +#elif defined(__APPLE__) || defined(__linux__) +#include +#include +#include +#endif + +#include "debug_system.h" + +/** + * @brief Class for crash reporting and minidump generation. + * + * 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 + } +}; + +// Convenience macro for simulating a crash (for testing) +#define SIMULATE_CRASH(message) CrashReporter::GetInstance().HandleCrash(message) diff --git a/attachments/simple_engine/debug_system.h b/attachments/simple_engine/debug_system.h new file mode 100644 index 00000000..d1c81b24 --- /dev/null +++ b/attachments/simple_engine/debug_system.h @@ -0,0 +1,252 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Enum for different log levels. + */ +enum class LogLevel { + Debug, + Info, + Warning, + Error, + Fatal +}; + +/** + * @brief Class for managing debugging and logging. + * + * 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; + +}; + +// Convenience macros for logging +#define LOG_DEBUG(tag, message) DebugSystem::GetInstance().Log(LogLevel::Debug, tag, message) +#define LOG_INFO(tag, message) DebugSystem::GetInstance().Log(LogLevel::Info, tag, message) +#define LOG_WARNING(tag, message) DebugSystem::GetInstance().Log(LogLevel::Warning, tag, message) +#define LOG_ERROR(tag, message) DebugSystem::GetInstance().Log(LogLevel::Error, tag, message) +#define LOG_FATAL(tag, message) DebugSystem::GetInstance().Log(LogLevel::Fatal, tag, message) + +// Convenience macros for performance measurement +#define MEASURE_START(name) DebugSystem::GetInstance().StartMeasurement(name) +#define MEASURE_END(name) DebugSystem::GetInstance().StopMeasurement(name) diff --git a/attachments/simple_engine/descriptor_manager.cpp b/attachments/simple_engine/descriptor_manager.cpp new file mode 100644 index 00000000..e75f7830 --- /dev/null +++ b/attachments/simple_engine/descriptor_manager.cpp @@ -0,0 +1,220 @@ +#include "descriptor_manager.h" +#include +#include +#include +#include "transform_component.h" +#include "camera_component.h" + +// Constructor +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; + } +} + +// 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::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; + } +} + +// 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)); +} + +// 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 + }; + + vk::raii::Buffer buffer(device.getDevice(), bufferInfo); + + // Allocate memory + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = device.findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + vk::raii::DeviceMemory bufferMemory(device.getDevice(), allocInfo); + + // Bind memory + buffer.bindMemory(*bufferMemory, 0); + + return {std::move(buffer), std::move(bufferMemory)}; +} diff --git a/attachments/simple_engine/descriptor_manager.h b/attachments/simple_engine/descriptor_manager.h new file mode 100644 index 00000000..48319b4d --- /dev/null +++ b/attachments/simple_engine/descriptor_manager.h @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#define GLM_FORCE_RADIANS +#include +#include + +#include "vulkan_device.h" +#include "entity.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; +}; + +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); +}; diff --git a/attachments/simple_engine/engine.cpp b/attachments/simple_engine/engine.cpp new file mode 100644 index 00000000..06113228 --- /dev/null +++ b/attachments/simple_engine/engine.cpp @@ -0,0 +1,876 @@ +#include "engine.h" +#include "scene_loading.h" +#include "mesh_component.h" + +#include +#include +#include +#include +#include +#include + +// This implementation corresponds to the Engine_Architecture chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc + +Engine::Engine() + : resourceManager(std::make_unique()), + modelLoader(std::make_unique()), + audioSystem(std::make_unique()), + physicsSystem(std::make_unique()), + imguiSystem(std::make_unique()) { +} + +Engine::~Engine() { + Cleanup(); +} + +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; +#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; + } + + // 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 a 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; + } + + // Initialize ImGui system + if (!imguiSystem->Initialize(renderer.get(), width, height)) { + return false; + } + + // Connect ImGui system to an 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; +#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; + currentFPS = framesSinceLastUpdate / fpsUpdateTimer; + // Average frame time in milliseconds over the last interval + double avgMs = (fpsUpdateTimer / static_cast(framesSinceLastUpdate)) * 1000.0; + + // 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(); + + // Clean up subsystems in reverse order of creation + imguiSystem.reset(); + physicsSystem.reset(); + audioSystem.reset(); + modelLoader.reset(); + renderer.reset(); + platform.reset(); + + initialized = false; + } +} + +Entity* Engine::CreateEntity(const std::string& name) { + // Always allow duplicate names; map stores a representative entity + // Create the entity + auto entity = std::make_unique(name); + // Add to the vector and map + entities.push_back(std::move(entity)); + Entity* rawPtr = entities.back().get(); + // Update the map to point to the most recently created entity with this name + entityMap[name] = rawPtr; + + return rawPtr; +} + +Entity* Engine::GetEntity(const std::string& name) { + auto it = entityMap.find(name); + if (it != entityMap.end()) { + return it->second; + } + return nullptr; +} + +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::find_if(entities.begin(), entities.end(), + [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::find_if(entities.begin(), entities.end(), + [&name](const std::unique_ptr& e) { + return e->GetName() == name; + }); + + if (remainingIt != entities.end()) { + entityMap[name] = remainingIt->get(); + } else { + entityMap.erase(name); + } + + return true; + } + + return false; +} + +bool Engine::RemoveEntity(const std::string& name) { + Entity* entity = GetEntity(name); + if (entity) { + return RemoveEntity(entity); + } + return false; +} + +void Engine::SetActiveCamera(CameraComponent* cameraComponent) { + activeCamera = cameraComponent; +} + +const CameraComponent* Engine::GetActiveCamera() const { + return activeCamera; +} + +const ResourceManager* Engine::GetResourceManager() const { + return resourceManager.get(); +} + +const Platform* Engine::GetPlatform() const { + return platform.get(); +} + +Renderer* Engine::GetRenderer() { + return renderer.get(); +} + +ModelLoader* Engine::GetModelLoader() { + return modelLoader.get(); +} + +const AudioSystem* Engine::GetAudioSystem() const { + return audioSystem.get(); +} + +PhysicsSystem* Engine::GetPhysicsSystem() { + return physicsSystem.get(); +} + +const ImGuiSystem* Engine::GetImGuiSystem() const { + return imguiSystem.get(); +} + + + +void Engine::handleMouseInput(float x, float y, uint32_t buttons) { + // Check if ImGui wants to capture mouse input first + bool imguiWantsMouse = imguiSystem && imguiSystem->WantCaptureMouse(); + + // Suppress right-click while loading + if (renderer && renderer->IsLoading()) { + buttons &= ~2u; // clear right button bit + } + + if (!imguiWantsMouse) { + // Handle mouse click for ball throwing (right mouse button) + if (buttons & 2) { // Right mouse button (bit 1) + if (!cameraControl.mouseRightPressed) { + cameraControl.mouseRightPressed = true; + // Throw a ball on mouse click + ThrowBall(x, y); + } + } else { + cameraControl.mouseRightPressed = false; + } + + // Handle camera rotation when left mouse button is pressed + if (buttons & 1) { // Left mouse button (bit 0) + if (!cameraControl.mouseLeftPressed) { + cameraControl.mouseLeftPressed = true; + cameraControl.firstMouse = true; + } + + if (cameraControl.firstMouse) { + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + cameraControl.firstMouse = false; + } + + float xOffset = x - cameraControl.lastMouseX; + float yOffset = cameraControl.lastMouseY - y; // Reversed since y-coordinates go from bottom to top + cameraControl.lastMouseX = x; + cameraControl.lastMouseY = y; + + xOffset *= cameraControl.mouseSensitivity; + yOffset *= cameraControl.mouseSensitivity; + + 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::Update(TimeDelta deltaTime) { + // Debug: Verify Update method is being called + static int updateCallCount = 0; + updateCallCount++; + // 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 + for (auto& entity : entities) { + if (entity->IsActive()) { + entity->Update(deltaTime); + } + } +} + +void Engine::Render() { + + // 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; +} + + +#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(); +} +#endif diff --git a/attachments/simple_engine/engine.h b/attachments/simple_engine/engine.h new file mode 100644 index 00000000..f8eede89 --- /dev/null +++ b/attachments/simple_engine/engine.h @@ -0,0 +1,358 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "platform.h" +#include "renderer.h" +#include "resource_manager.h" +#include "entity.h" +#include "camera_component.h" +#include "model_loader.h" +#include "audio_system.h" +#include "physics_system.h" +#include "imgui_system.h" + +/** + * @brief Main engine class that manages the game loop and subsystems. + * + * This class implements the core engine architecture as described in the Engine_Architecture chapter: + * @see en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc + */ +class Engine { +public: + using TimeDelta = std::chrono::milliseconds; + /** + * @brief Default constructor. + */ + Engine(); + + /** + * @brief Destructor for proper cleanup. + */ + ~Engine(); + + /** + * @brief Initialize the engine. + * @param appName The name of the application. + * @param width The width of the window. + * @param height The height of the window. + * @param enableValidationLayers Whether to enable Vulkan validation layers. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& appName, int width, int height, bool enableValidationLayers = true); + + /** + * @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); + +#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(); +#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); + +}; diff --git a/attachments/simple_engine/entity.cpp b/attachments/simple_engine/entity.cpp new file mode 100644 index 00000000..41292561 --- /dev/null +++ b/attachments/simple_engine/entity.cpp @@ -0,0 +1,30 @@ +#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::Update(std::chrono::milliseconds deltaTime) { + if (!active) return; + + for (auto& component : components) { + if (component->IsActive()) { + component->Update(deltaTime); + } + } +} + +void Entity::Render() { + if (!active) return; + + for (auto& component : components) { + if (component->IsActive()) { + component->Render(); + } + } +} diff --git a/attachments/simple_engine/entity.h b/attachments/simple_engine/entity.h new file mode 100644 index 00000000..0ef252f2 --- /dev/null +++ b/attachments/simple_engine/entity.h @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "component.h" + +/** + * @brief Entity class that can have multiple components attached to it. + * + * 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; + } +}; diff --git a/attachments/simple_engine/fetch_bistro_assets.bat b/attachments/simple_engine/fetch_bistro_assets.bat new file mode 100644 index 00000000..fc4941bb --- /dev/null +++ b/attachments/simple_engine/fetch_bistro_assets.bat @@ -0,0 +1,61 @@ +@echo off +setlocal enabledelayedexpansion + +REM Fetch the Bistro example assets into the desired assets directory. +REM Default target when run from attachments\simple_engine: Assets\bistro +REM Usage: +REM fetch_bistro_assets.bat [target-dir] +REM Example: +REM fetch_bistro_assets.bat + +set REPO_SSH=git@github.com:gpx1000/bistro.git +set REPO_HTTPS=https://github.com/gpx1000/bistro.git + +if "%~1"=="" ( + set "TARGET_DIR=Assets\bistro" +) else ( + set "TARGET_DIR=%~1" +) + +REM Ensure parent directory exists (avoid trailing backslash quoting issue by appending a dot) +for %%I in ("%TARGET_DIR%") do set "PARENT=%%~dpI" +if not exist "%PARENT%." mkdir "%PARENT%." + +REM If directory exists and is a git repo, update it; otherwise clone it +if exist "%TARGET_DIR%\.git" ( + echo Updating existing bistro assets in %TARGET_DIR% + pushd "%TARGET_DIR%" >nul 2>nul + git pull --ff-only + if errorlevel 1 ( + echo ERROR: Failed to update repository at %TARGET_DIR%. + popd >nul 2>nul + endlocal & exit /b 1 + ) + popd >nul 2>nul +) else ( + echo Cloning bistro assets into %TARGET_DIR% + REM Try SSH first; fall back to HTTPS on failure + git clone --depth 1 "%REPO_SSH%" "%TARGET_DIR%" 1>nul 2>nul + if errorlevel 1 ( + echo SSH clone failed, trying HTTPS + git clone --depth 1 "%REPO_HTTPS%" "%TARGET_DIR%" + if errorlevel 1 ( + echo ERROR: Failed to clone repository via HTTPS into %TARGET_DIR%. + endlocal & exit /b 1 + ) + ) +) + +REM If git-lfs is available, ensure LFS content is pulled (ignore failures) +if exist "%TARGET_DIR%\.git" ( + pushd "%TARGET_DIR%" >nul 2>nul + git lfs version >nul 2>nul + if not errorlevel 1 ( + git lfs install --local >nul 2>nul + git lfs pull || rem ignore + ) + popd >nul 2>nul +) + +echo Bistro assets ready at: %TARGET_DIR% +endlocal & exit /b 0 diff --git a/attachments/simple_engine/fetch_bistro_assets.sh b/attachments/simple_engine/fetch_bistro_assets.sh new file mode 100755 index 00000000..98fc829b --- /dev/null +++ b/attachments/simple_engine/fetch_bistro_assets.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fetch the Bistro example assets into the desired assets directory. +# Default target: assets/bistro at the repository root. +# Usage: +# ./fetch_bistro_assets.sh [target-dir] +# Example: +# ./fetch_bistro_assets.sh # clones to assets/bistro + +REPO_SSH="git@github.com:gpx1000/bistro.git" +REPO_HTTPS="https://github.com/gpx1000/bistro.git" +TARGET_DIR="${1:-Assets/bistro}" + +mkdir -p "$(dirname "${TARGET_DIR}")" + +# If directory exists and is a git repo, update it; otherwise clone it. +if [ -d "${TARGET_DIR}/.git" ]; then + echo "Updating existing bistro assets in ${TARGET_DIR}" + git -C "${TARGET_DIR}" pull --ff-only +else + echo "Cloning bistro assets into ${TARGET_DIR}" + # Try SSH first; if it fails (e.g., no SSH key), fall back to HTTPS. + if git clone --depth 1 "${REPO_SSH}" "${TARGET_DIR}" 2>/dev/null; then + : + else + echo "SSH clone failed, trying HTTPS" + git clone --depth 1 "${REPO_HTTPS}" "${TARGET_DIR}" + fi +fi + +# If git-lfs is available, ensure LFS content is pulled +if command -v git >/dev/null 2>&1 && git -C "${TARGET_DIR}" lfs version >/dev/null 2>&1; then + git -C "${TARGET_DIR}" lfs install --local >/dev/null 2>&1 || true + git -C "${TARGET_DIR}" lfs pull || true +fi + +echo "Bistro assets ready at: ${TARGET_DIR}" diff --git a/attachments/simple_engine/imgui/imconfig.h b/attachments/simple_engine/imgui/imconfig.h new file mode 100644 index 00000000..33cbadd1 --- /dev/null +++ b/attachments/simple_engine/imgui/imconfig.h @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------------- +// USER IMPLEMENTATION +// This file contains compile-time options for ImGui. +// Other options (memory allocation overrides, callbacks, etc.) can be set at runtime via the ImGuiIO structure - ImGui::GetIO(). +//----------------------------------------------------------------------------- + +#pragma once + +//---- Define assertion handler. Defaults to calling assert(). +//#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 ) + +//---- Include imgui_user.h at the end of imgui.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 + +//---- Don't implement help and test window functionality (ShowUserGuide()/ShowStyleEditor()/ShowTestWindow() methods will be empty) +//#define IMGUI_DISABLE_TEST_WINDOWS + +//---- Don't define obsolete functions names +//#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +//---- Implement STB libraries in a namespace to avoid conflicts +//#define IMGUI_STB_NAMESPACE ImGuiStb + +//---- Define constructor and implicit cast operators to convert back<>forth from your math types and ImVec2/ImVec4. +/* +#define IM_VEC2_CLASS_EXTRA \ + ImVec2(const MyVec2& f) { x = f.x; y = f.y; } \ + operator MyVec2() const { return MyVec2(x,y); } + +#define IM_VEC4_CLASS_EXTRA \ + ImVec4(const MyVec4& f) { x = f.x; y = f.y; z = f.z; w = f.w; } \ + operator MyVec4() const { return MyVec4(x,y,z,w); } +*/ + +//---- Tip: You can add extra functions within the ImGui:: namespace, here or in your own headers files. +//---- e.g. create variants of the ImGui::Value() helper for your low-level math types, or your own widgets/helpers. +/* +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 new file mode 100644 index 00000000..c53edb84 --- /dev/null +++ b/attachments/simple_engine/imgui/imgui.cpp @@ -0,0 +1,13278 @@ +// dear imgui, v1.60 WIP +// (main code and documentation) + +// Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp for demo code. +// Newcomers, read 'Programmer guide' below for notes on how to setup Dear ImGui in your codebase. +// Get latest version at https://github.com/ocornut/imgui +// Releases change-log at https://github.com/ocornut/imgui/releases +// Gallery (please post your screenshots/video there!): https://github.com/ocornut/imgui/issues/1269 +// Developed by Omar Cornut and every direct or indirect contributors to the GitHub. +// This library is free but I need your support to sustain development and maintenance. +// If you work for a company, please consider financial support, see Readme. For individuals: https://www.patreon.com/imgui + +/* + + Index + - MISSION STATEMENT + - END-USER GUIDE + - PROGRAMMER GUIDE (read me!) + - Read first + - How to update to a newer version of Dear ImGui + - Getting started with integrating Dear ImGui in your code/engine + - Using gamepad/keyboard navigation [BETA] + - API BREAKING CHANGES (read me when you update!) + - ISSUES & TODO LIST + - FREQUENTLY ASKED QUESTIONS (FAQ), TIPS + - How can I help? + - How can I display an image? What is ImTextureID, how does it works? + - How can I have multiple widgets with the same label? Can I have widget without a label? (Yes). A primer on labels and the ID stack. + - How can I tell when Dear ImGui wants my mouse/keyboard inputs VS when I can pass them to my application? + - How can I load a different font than the default? + - How can I easily use icons in my application? + - How can I load multiple fonts? + - How can I display and input non-latin characters such as Chinese, Japanese, Korean, Cyrillic? + - How can I preserve my Dear ImGui context across reloading a DLL? (loss of the global/static variables) + - How can I use the drawing facilities without an ImGui window? (using ImDrawList API) + - I integrated Dear ImGui in my engine and the text or lines are blurry.. + - I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around.. + - ISSUES & TODO-LIST + - CODE + + + MISSION STATEMENT + ================= + + - Easy to use to create code-driven and data-driven tools + - Easy to use to create ad hoc short-lived tools and long-lived, more elaborate tools + - Easy to hack and improve + - Minimize screen real-estate usage + - Minimize setup and maintenance + - Minimize state storage on user side + - Portable, minimize dependencies, run on target (consoles, phones, etc.) + - Efficient runtime and memory consumption (NB- we do allocate when "growing" content e.g. creating a window, opening a tree node + for the first time, etc. but a typical frame won't allocate anything) + + Designed for developers and content-creators, not the typical end-user! Some of the weaknesses includes: + - Doesn't look fancy, doesn't animate + - Limited layout features, intricate layouts are typically crafted in code + + + END-USER GUIDE + ============== + + - Double-click on title bar to collapse window. + - Click upper right corner to close a window, available when 'bool* p_open' is passed to ImGui::Begin(). + - Click and drag on lower right corner to resize window (double-click to auto fit window to its contents). + - Click and drag on any empty space to move window. + - TAB/SHIFT+TAB to cycle through keyboard editable fields. + - CTRL+Click on a slider or drag box to input value as text. + - Use mouse wheel to scroll. + - Text editor: + - Hold SHIFT or use mouse to select text. + - CTRL+Left/Right to word jump. + - CTRL+Shift+Left/Right to select words. + - CTRL+A our Double-Click to select all. + - CTRL+X,CTRL+C,CTRL+V to use OS clipboard/ + - CTRL+Z,CTRL+Y to undo/redo. + - ESCAPE to revert text to its original value. + - You can apply arithmetic operators +,*,/ on numerical values. Use +- to subtract (because - would set a negative value!) + - Controls are automatically adjusted for OSX to match standard OSX text editing operations. + - Gamepad navigation: see suggested mappings in imgui.h ImGuiNavInput_ + + + PROGRAMMER GUIDE + ================ + + READ FIRST + + - Read the FAQ below this section! + - Your code creates the UI, if your code doesn't run the UI is gone! == very dynamic UI, no construction/destructions steps, less data retention + on your side, no state duplication, less sync, less bugs. + - Call and read ImGui::ShowDemoWindow() for demo code demonstrating most features. + - You can learn about immediate-mode gui principles at http://www.johno.se/book/imgui.html or watch http://mollyrocket.com/861 + + 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. + If a function/type has been renamed / or marked obsolete, try to fix the name in your code before it is permanently removed from the public API. + If you have a problem with a missing function/symbols, search for its name in the code, there will likely be a comment about it. + Please report any issue to the GitHub page! + - Try to keep your copy of dear imgui reasonably up to date. + + GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + + - Add the Dear ImGui source files to your projects, using your preferred build system. + It is recommended you build the .cpp files as part of your project and not as a library. + - You can later customize the imconfig.h file to tweak some compilation time behavior, such as integrating imgui types with your own maths types. + - See examples/ folder for standalone sample applications. + - You may be able to grab and copy a ready made imgui_impl_*** file from the examples/. + - 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 + integration you don't need to worry about it all. + - Init: call io.Fonts->GetTexDataAsRGBA32(...), it will build the font atlas texture, then load the texture pixels into graphics memory. + - Every frame: + - In your main loop as early a possible, fill the IO fields marked 'Input' (e.g. mouse position, buttons, keyboard info, etc.) + - Call ImGui::NewFrame() to begin the frame + - You can use any ImGui function you want between NewFrame() and Render() + - Call ImGui::Render() as late as you can to end the frame and finalize render data. it will call your io.RenderDrawListFn handler. + (Even if you don't render, call Render() and ignore the callback, or call EndFrame() instead. Otherwhise some features will break) + - All rendering information are stored into command-lists until ImGui::Render() is called. + - Dear ImGui never touches or knows about your GPU state. the only function that knows about GPU is the RenderDrawListFn handler that you provide. + - Effectively it means you can create widgets at any time in your code, regardless of considerations of being in "update" vs "render" phases + of your own application. + - Refer to the examples applications in the examples/ folder for instruction on how to setup your code. + - A minimal application skeleton may be: + + // Application init + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize.x = 1920.0f; + io.DisplaySize.y = 1280.0f; + // TODO: Fill others settings of the io structure later. + + // Load texture atlas (there is a default font so you don't need to care about choosing a font yet) + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(pixels, &width, &height); + // TODO: At this points you've got the texture data and you need to upload that your your graphic system: + MyTexture* texture = MyEngine::CreateTextureFromMemoryPixels(pixels, width, height, TEXTURE_TYPE_RGBA) + // TODO: Store your texture pointer/identifier (whatever your engine uses) in 'io.Fonts->TexID'. This will be passed back to your via the renderer. + io.Fonts->TexID = (void*)texture; + + // Application main loop + while (true) + { + // Setup low-level inputs (e.g. on Win32, GetKeyboardState(), or write to those fields from your Windows message loop handlers, etc.) + ImGuiIO& io = ImGui::GetIO(); + io.DeltaTime = 1.0f/60.0f; + io.MousePos = mouse_pos; + io.MouseDown[0] = mouse_button_0; + io.MouseDown[1] = mouse_button_1; + + // Call NewFrame(), after this point you can use ImGui::* functions anytime + ImGui::NewFrame(); + + // Most of your application code here + MyGameUpdate(); // may use any ImGui functions, e.g. ImGui::Begin("My window"); ImGui::Text("Hello, world!"); ImGui::End(); + MyGameRender(); // may use any ImGui functions as well! + + // Render & swap video buffers + ImGui::Render(); + MyImGuiRenderFunction(ImGui::GetDrawData()); + SwapBuffers(); + } + + // Shutdown + ImGui::DestroyContext(); + + + - A minimal render function skeleton may be: + + void void MyRenderFunction(ImDrawData* draw_data) + { + // TODO: Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled + // TODO: Setup viewport, orthographic projection matrix + // TODO: Setup shader: vertex { float2 pos, float2 uv, u32 color }, fragment shader sample color from 1 texture, multiply by vertex color. + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data; // vertex buffer generated by ImGui + const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data; // index buffer generated by ImGui + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback) + { + pcmd->UserCallback(cmd_list, pcmd); + } + else + { + // The texture for the draw call is specified by pcmd->TextureId. + // The vast majority of draw calls with use the imgui texture atlas, which value you have set yourself during initialization. + MyEngineBindTexture(pcmd->TextureId); + + // We are using scissoring to clip some objects. All low-level graphics API supports it. + // If your engine doesn't support scissoring yet, you will get some small glitches (some elements outside their bounds) which you can fix later. + MyEngineScissor((int)pcmd->ClipRect.x, (int)pcmd->ClipRect.y, (int)(pcmd->ClipRect.z - pcmd->ClipRect.x), (int)(pcmd->ClipRect.w - pcmd->ClipRect.y)); + + // Render 'pcmd->ElemCount/3' indexed triangles. + // By default the indices ImDrawIdx are 16-bits, you can change them to 32-bits if your engine doesn't support 16-bits indices. + MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer, vtx_buffer); + } + idx_buffer += pcmd->ElemCount; + } + } + } + + - The examples/ folders contains many functional implementation of the pseudo-code above. + - When calling NewFrame(), the 'io.WantCaptureMouse'/'io.WantCaptureKeyboard'/'io.WantTextInput' flags are updated. + They tell you if ImGui intends to use your inputs. So for example, if 'io.WantCaptureMouse' is set you would typically want to hide + mouse inputs from the rest of your application. Read the FAQ below for more information about those flags. + + USING GAMEPAD/KEYBOARD NAVIGATION [BETA] + + - Ask questions and report issues at https://github.com/ocornut/imgui/issues/787 + - The initial focus was to support game controllers, but keyboard is becoming increasingly and decently usable. + - Keyboard: + - Set io.NavFlags |= ImGuiNavFlags_EnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.KeyDown[] + io.KeyMap[] arrays. + - When keyboard navigation is active (io.NavActive + NavFlags_EnableKeyboard), the io.WantCaptureKeyboard flag will be set. + For more advanced uses, you may want to read from: + - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set. + - io.NavVisible: true when the navigation cursor is visible (and usually goes false when mouse is used). + - or query focus information with e.g. IsWindowFocused(), IsItemFocused() etc. functions. + Please reach out if you think the game vs navigation input sharing could be improved. + - Gamepad: + - Set io.NavFlags |= ImGuiNavFlags_EnableGamepad to enable. Fill the io.NavInputs[] fields before calling NewFrame(). Note that io.NavInputs[] is cleared by EndFrame(). + - See 'enum ImGuiNavInput_' in imgui.h for a description of inputs. For each entry of io.NavInputs[], set the following values: + 0.0f= not held. 1.0f= fully held. Pass intermediate 0.0f..1.0f values for analog triggers/sticks. + - We uses a simple >0.0f test for activation testing, and won't attempt to test for a dead-zone. + Your code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, maybe a power curve, etc.). + - If you need to share inputs between your game and the imgui parts, the easiest approach is to go all-or-nothing, with a buttons combo to toggle the target. + Please reach out if you think the game vs navigation input sharing could be improved. + - Mouse: + - PS4 users: Consider emulating a mouse cursor with DualShock4 touch pad or a spare analog stick as a mouse-emulation fallback. + - Consoles/Tablet/Phone users: Consider using Synergy host (on your computer) + uSynergy.c (in your console/tablet/phone app) to use your PC mouse/keyboard. + - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the ImGuiNavFlags_MoveMouse flag in io.NavFlags. + Enabling ImGuiNavFlags_MoveMouse instructs dear imgui to move your mouse cursor along with navigation movements. + When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantMoveMouse' to notify you that it wants the mouse cursor to be moved. + When that happens your back-end NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the binding in examples/ do that. + (If you set the ImGuiNavFlags_MoveMouse flag but don't honor 'io.WantMoveMouse' properly, imgui will misbehave as it will see your mouse as moving back and forth.) + (In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want + to set a boolean to ignore your other external mouse positions until the external source is moved again.) + + + API BREAKING CHANGES + ==================== + + Occasionally introducing changes that are breaking the API. The breakage are generally minor and easy to fix. + Here is a change-log of API breaking changes, if you are using one of the functions listed, expect to have to fix some code. + Also read releases logs https://github.com/ocornut/imgui/releases for more details. + + - 2018/02/18 (1.60) - BeginDragDropSource(): temporarily removed the optional mouse_button=0 parameter because it is not really usable in many situations at the moment. + - 2018/02/16 (1.60) - obsoleted the io.RenderDrawListsFn callback, you can call your graphics engine render function after ImGui::Render(). Use ImGui::GetDrawData() to retrieve the ImDrawData* to display. + - 2018/02/07 (1.60) - reorganized context handling to be more explicit, + - YOU NOW NEED TO CALL ImGui::CreateContext() AT THE BEGINNING OF YOUR APP, AND CALL ImGui::DestroyContext() AT THE END. + - removed Shutdown() function, as DestroyContext() serve this purpose. + - you may pass a ImFontAtlas* pointer to CreateContext() to share a font atlas between contexts. Otherwhise CreateContext() will create its own font atlas instance. + - removed allocator parameters from CreateContext(), they are now setup with SetAllocatorFunctions(), and shared by all contexts. + - removed the default global context and font atlas instance, which were confusing for users of DLL reloading and users of multiple contexts. + - 2018/01/31 (1.60) - moved sample TTF files from extra_fonts/ to misc/fonts/. If you loaded files directly from the imgui repo you may need to update your paths. + - 2018/01/11 (1.60) - obsoleted IsAnyWindowHovered() in favor of IsWindowHovered(ImGuiHoveredFlags_AnyWindow). Kept redirection function (will obsolete). + - 2018/01/11 (1.60) - obsoleted IsAnyWindowFocused() in favor of IsWindowFocused(ImGuiFocusedFlags_AnyWindow). Kept redirection function (will obsolete). + - 2018/01/03 (1.60) - renamed ImGuiSizeConstraintCallback to ImGuiSizeCallback, ImGuiSizeConstraintCallbackData to ImGuiSizeCallbackData. + - 2017/12/29 (1.60) - removed CalcItemRectClosestPoint() which was weird and not really used by anyone except demo code. If you need it it's easy to replicate on your side. + - 2017/12/24 (1.53) - renamed the emblematic ShowTestWindow() function to ShowDemoWindow(). Kept redirection function (will obsolete). + - 2017/12/21 (1.53) - ImDrawList: renamed style.AntiAliasedShapes to style.AntiAliasedFill for consistency and as a way to explicitly break code that manipulate those flag at runtime. You can now manipulate ImDrawList::Flags + - 2017/12/21 (1.53) - ImDrawList: removed 'bool anti_aliased = true' final parameter of ImDrawList::AddPolyline() and ImDrawList::AddConvexPolyFilled(). Prefer manipulating ImDrawList::Flags if you need to toggle them during the frame. + - 2017/12/14 (1.53) - using the ImGuiWindowFlags_NoScrollWithMouse flag on a child window forwards the mouse wheel event to the parent window, unless either ImGuiWindowFlags_NoInputs or ImGuiWindowFlags_NoScrollbar are also set. + - 2017/12/13 (1.53) - renamed GetItemsLineHeightWithSpacing() to GetFrameHeightWithSpacing(). Kept redirection function (will obsolete). + - 2017/12/13 (1.53) - obsoleted IsRootWindowFocused() in favor of using IsWindowFocused(ImGuiFocusedFlags_RootWindow). Kept redirection function (will obsolete). + - obsoleted IsRootWindowOrAnyChildFocused() in favor of using IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows). Kept redirection function (will obsolete). + - 2017/12/12 (1.53) - renamed ImGuiTreeNodeFlags_AllowOverlapMode to ImGuiTreeNodeFlags_AllowItemOverlap. Kept redirection enum (will obsolete). + - 2017/12/10 (1.53) - removed SetNextWindowContentWidth(), prefer using SetNextWindowContentSize(). Kept redirection function (will obsolete). + - 2017/11/27 (1.53) - renamed ImGuiTextBuffer::append() helper to appendf(), appendv() to appendfv(). If you copied the 'Log' demo in your code, it uses appendv() so that needs to be renamed. + - 2017/11/18 (1.53) - Style, Begin: removed ImGuiWindowFlags_ShowBorders window flag. Borders are now fully set up in the ImGuiStyle structure (see e.g. style.FrameBorderSize, style.WindowBorderSize). Use ImGui::ShowStyleEditor() to look them up. + Please note that the style system will keep evolving (hopefully stabilizing in Q1 2018), and so custom styles will probably subtly break over time. It is recommended you use the StyleColorsClassic(), StyleColorsDark(), StyleColorsLight() functions. + - 2017/11/18 (1.53) - Style: removed ImGuiCol_ComboBg in favor of combo boxes using ImGuiCol_PopupBg for consistency. + - 2017/11/18 (1.53) - Style: renamed ImGuiCol_ChildWindowBg to ImGuiCol_ChildBg. + - 2017/11/18 (1.53) - Style: renamed style.ChildWindowRounding to style.ChildRounding, ImGuiStyleVar_ChildWindowRounding to ImGuiStyleVar_ChildRounding. + - 2017/11/02 (1.53) - obsoleted IsRootWindowOrAnyChildHovered() in favor of using IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); + - 2017/10/24 (1.52) - renamed IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCS/IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCS to IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS/IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS for consistency. + - 2017/10/20 (1.52) - changed IsWindowHovered() default parameters behavior to return false if an item is active in another window (e.g. click-dragging item from another window to this window). You can use the newly introduced IsWindowHovered() flags to requests this specific behavior if you need it. + - 2017/10/20 (1.52) - marked IsItemHoveredRect()/IsMouseHoveringWindow() as obsolete, in favor of using the newly introduced flags for IsItemHovered() and IsWindowHovered(). See https://github.com/ocornut/imgui/issues/1382 for details. + removed the IsItemRectHovered()/IsWindowRectHovered() names introduced in 1.51 since they were merely more consistent names for the two functions we are now obsoleting. + - 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/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)! + - renamed IsMouseHoveringAnyWindow() to IsAnyWindowHovered() for consistency. Kept inline redirection function (will obsolete). + - renamed IsMouseHoveringWindow() to IsWindowRectHovered() for consistency. Kept inline redirection function (will obsolete). + - 2017/08/20 (1.51) - renamed GetStyleColName() to GetStyleColorName() for consistency. + - 2017/08/20 (1.51) - added PushStyleColor(ImGuiCol idx, ImU32 col) overload, which _might_ cause an "ambiguous call" compilation error if you are using ImColor() with implicit cast. Cast to ImU32 or ImVec4 explicily to fix. + - 2017/08/15 (1.51) - marked the weird IMGUI_ONCE_UPON_A_FRAME helper macro as obsolete. prefer using the more explicit ImGuiOnceUponAFrame. + - 2017/08/15 (1.51) - changed parameter order for BeginPopupContextWindow() from (const char*,int buttons,bool also_over_items) to (const char*,int buttons,bool also_over_items). Note that most calls relied on default parameters completely. + - 2017/08/13 (1.51) - renamed ImGuiCol_Columns*** to ImGuiCol_Separator***. Kept redirection enums (will obsolete). + - 2017/08/11 (1.51) - renamed ImGuiSetCond_*** types and flags to ImGuiCond_***. Kept redirection enums (will obsolete). + - 2017/08/09 (1.51) - removed ValueColor() helpers, they are equivalent to calling Text(label) + SameLine() + ColorButton(). + - 2017/08/08 (1.51) - removed ColorEditMode() and ImGuiColorEditMode in favor of ImGuiColorEditFlags and parameters to the various Color*() functions. The SetColorEditOptions() allows to initialize default but the user can still change them with right-click context menu. + - changed prototype of 'ColorEdit4(const char* label, float col[4], bool show_alpha = true)' to 'ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0)', where passing flags = 0x01 is a safe no-op (hello dodgy backward compatibility!). - check and run the demo window, under "Color/Picker Widgets", to understand the various new options. + - changed prototype of rarely used 'ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)' to 'ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0,0))' + - 2017/07/20 (1.51) - removed IsPosHoveringAnyWindow(ImVec2), which was partly broken and misleading. ASSERT + redirect user to io.WantCaptureMouse + - 2017/05/26 (1.50) - removed ImFontConfig::MergeGlyphCenterV in favor of a more multipurpose ImFontConfig::GlyphOffset. + - 2017/05/01 (1.50) - renamed ImDrawList::PathFill() (rarely used directly) to ImDrawList::PathFillConvex() for clarity. + - 2016/11/06 (1.50) - BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetId() and use it instead of passing string to BeginChild(). + - 2016/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. + However if your TitleBg/TitleBgActive alpha was <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. + This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color. + ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) + { + float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)), k = title_bg_col.w / new_a; + return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); + } + If this is confusing, pick the RGB value from title bar from an old screenshot and apply this as TitleBg/TitleBgActive. Or you may just create TitleBgActive from a tweaked TitleBg color. + - 2016/05/07 (1.49) - removed confusing set of GetInternalState(), GetInternalStateSize(), SetInternalState() functions. Now using CreateContext(), DestroyContext(), GetCurrentContext(), SetCurrentContext(). + - 2016/05/02 (1.49) - renamed SetNextTreeNodeOpened() to SetNextTreeNodeOpen(), no redirection. + - 2016/05/01 (1.49) - obsoleted old signature of CollapsingHeader(const char* label, const char* str_id = NULL, bool display_frame = true, bool default_open = false) as extra parameters were badly designed and rarely used. You can replace the "default_open = true" flag in new API with CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen). + - 2016/04/26 (1.49) - changed ImDrawList::PushClipRect(ImVec4 rect) to ImDraw::PushClipRect(Imvec2 min,ImVec2 max,bool intersect_with_current_clip_rect=false). Note that higher-level ImGui::PushClipRect() is preferable because it will clip at logic/widget level, whereas ImDrawList::PushClipRect() only affect your renderer. + - 2016/04/03 (1.48) - removed style.WindowFillAlphaDefault setting which was redundant. Bake default BG alpha inside style.Colors[ImGuiCol_WindowBg] and all other Bg color values. (ref github issue #337). + - 2016/04/03 (1.48) - renamed ImGuiCol_TooltipBg to ImGuiCol_PopupBg, used by popups/menus and tooltips. popups/menus were previously using ImGuiCol_WindowBg. (ref github issue #337) + - 2016/03/21 (1.48) - renamed GetWindowFont() to GetFont(), GetWindowFontSize() to GetFontSize(). Kept inline redirection function (will obsolete). + - 2016/03/02 (1.48) - InputText() completion/history/always callbacks: if you modify the text buffer manually (without using DeleteChars()/InsertChars() helper) you need to maintain the BufTextLen field. added an assert. + - 2016/01/23 (1.48) - fixed not honoring exact width passed to PushItemWidth(), previously it would add extra FramePadding.x*2 over that width. if you had manual pixel-perfect alignment in place it might affect you. + - 2015/12/27 (1.48) - fixed ImDrawList::AddRect() which used to render a rectangle 1 px too large on each axis. + - 2015/12/04 (1.47) - renamed Color() helpers to ValueColor() - dangerously named, rarely used and probably to be made obsolete. + - 2015/08/29 (1.45) - with the addition of horizontal scrollbar we made various fixes to inconsistencies with dealing with cursor position. + GetCursorPos()/SetCursorPos() functions now include the scrolled amount. It shouldn't affect the majority of users, but take note that SetCursorPosX(100.0f) puts you at +100 from the starting x position which may include scrolling, not at +100 from the window left side. + GetContentRegionMax()/GetWindowContentRegionMin()/GetWindowContentRegionMax() functions allow include the scrolled amount. Typically those were used in cases where no scrolling would happen so it may not be a problem, but watch out! + - 2015/08/29 (1.45) - renamed style.ScrollbarWidth to style.ScrollbarSize + - 2015/08/05 (1.44) - split imgui.cpp into extra files: imgui_demo.cpp imgui_draw.cpp imgui_internal.h that you need to add to your project. + - 2015/07/18 (1.44) - fixed angles in ImDrawList::PathArcTo(), PathArcToFast() (introduced in 1.43) being off by an extra PI for no justifiable reason + - 2015/07/14 (1.43) - add new ImFontAtlas::AddFont() API. For the old AddFont***, moved the 'font_no' parameter of ImFontAtlas::AddFont** functions to the ImFontConfig structure. + you need to render your textured triangles with bilinear filtering to benefit from sub-pixel positioning of text. + - 2015/07/08 (1.43) - switched rendering data to use indexed rendering. this is saving a fair amount of CPU/GPU and enables us to get anti-aliasing for a marginal cost. + this necessary change will break your rendering function! the fix should be very easy. sorry for that :( + - if you are using a vanilla copy of one of the imgui_impl_XXXX.cpp provided in the example, you just need to update your copy and you can ignore the rest. + - the signature of the io.RenderDrawListsFn handler has changed! + ImGui_XXXX_RenderDrawLists(ImDrawList** const cmd_lists, int cmd_lists_count) + became: + ImGui_XXXX_RenderDrawLists(ImDrawData* draw_data). + argument 'cmd_lists' -> 'draw_data->CmdLists' + argument 'cmd_lists_count' -> 'draw_data->CmdListsCount' + ImDrawList 'commands' -> 'CmdBuffer' + ImDrawList 'vtx_buffer' -> 'VtxBuffer' + ImDrawList n/a -> 'IdxBuffer' (new) + ImDrawCmd 'vtx_count' -> 'ElemCount' + ImDrawCmd 'clip_rect' -> 'ClipRect' + ImDrawCmd 'user_callback' -> 'UserCallback' + ImDrawCmd 'texture_id' -> 'TextureId' + - each ImDrawList now contains both a vertex buffer and an index buffer. For each command, render ElemCount/3 triangles using indices from the index buffer. + - if you REALLY cannot render indexed primitives, you can call the draw_data->DeIndexAllBuffers() method to de-index the buffers. This is slow and a waste of CPU/GPU. Prefer using indexed rendering! + - refer to code in the examples/ folder or ask on the GitHub if you are unsure of how to upgrade. please upgrade! + - 2015/07/10 (1.43) - changed SameLine() parameters from int to float. + - 2015/07/02 (1.42) - renamed SetScrollPosHere() to SetScrollFromCursorPos(). Kept inline redirection function (will obsolete). + - 2015/07/02 (1.42) - renamed GetScrollPosY() to GetScrollY(). Necessary to reduce confusion along with other scrolling functions, because positions (e.g. cursor position) are not equivalent to scrolling amount. + - 2015/06/14 (1.41) - changed ImageButton() default bg_col parameter from (0,0,0,1) (black) to (0,0,0,0) (transparent) - makes a difference when texture have transparence + - 2015/06/14 (1.41) - changed Selectable() API from (label, selected, size) to (label, selected, flags, size). Size override should have been rarely be used. Sorry! + - 2015/05/31 (1.40) - renamed GetWindowCollapsed() to IsWindowCollapsed() for consistency. Kept inline redirection function (will obsolete). + - 2015/05/31 (1.40) - renamed IsRectClipped() to IsRectVisible() for consistency. Note that return value is opposite! Kept inline redirection function (will obsolete). + - 2015/05/27 (1.40) - removed the third 'repeat_if_held' parameter from Button() - sorry! it was rarely used and inconsistent. Use PushButtonRepeat(true) / PopButtonRepeat() to enable repeat on desired buttons. + - 2015/05/11 (1.40) - changed BeginPopup() API, takes a string identifier instead of a bool. ImGui needs to manage the open/closed state of popups. Call OpenPopup() to actually set the "open" state of a popup. BeginPopup() returns true if the popup is opened. + - 2015/05/03 (1.40) - removed style.AutoFitPadding, using style.WindowPadding makes more sense (the default values were already the same). + - 2015/04/13 (1.38) - renamed IsClipped() to IsRectClipped(). Kept inline redirection function until 1.50. + - 2015/04/09 (1.38) - renamed ImDrawList::AddArc() to ImDrawList::AddArcFast() for compatibility with future API + - 2015/04/03 (1.38) - removed ImGuiCol_CheckHovered, ImGuiCol_CheckActive, replaced with the more general ImGuiCol_FrameBgHovered, ImGuiCol_FrameBgActive. + - 2014/04/03 (1.38) - removed support for passing -FLT_MAX..+FLT_MAX as the range for a SliderFloat(). Use DragFloat() or Inputfloat() instead. + - 2015/03/17 (1.36) - renamed GetItemBoxMin()/GetItemBoxMax()/IsMouseHoveringBox() to GetItemRectMin()/GetItemRectMax()/IsMouseHoveringRect(). Kept inline redirection function until 1.50. + - 2015/03/15 (1.36) - renamed style.TreeNodeSpacing to style.IndentSpacing, ImGuiStyleVar_TreeNodeSpacing to ImGuiStyleVar_IndentSpacing + - 2015/03/13 (1.36) - renamed GetWindowIsFocused() to IsWindowFocused(). Kept inline redirection function until 1.50. + - 2015/03/08 (1.35) - renamed style.ScrollBarWidth to style.ScrollbarWidth (casing) + - 2015/02/27 (1.34) - renamed OpenNextNode(bool) to SetNextTreeNodeOpened(bool, ImGuiSetCond). Kept inline redirection function until 1.50. + - 2015/02/27 (1.34) - renamed ImGuiSetCondition_*** to ImGuiSetCond_***, and _FirstUseThisSession becomes _Once. + - 2015/02/11 (1.32) - changed text input callback ImGuiTextEditCallback return type from void-->int. reserved for future use, return 0 for now. + - 2015/02/10 (1.32) - renamed GetItemWidth() to CalcItemWidth() to clarify its evolving behavior + - 2015/02/08 (1.31) - renamed GetTextLineSpacing() to GetTextLineHeightWithSpacing() + - 2015/02/01 (1.31) - removed IO.MemReallocFn (unused) + - 2015/01/19 (1.30) - renamed ImGuiStorage::GetIntPtr()/GetFloatPtr() to GetIntRef()/GetIntRef() because Ptr was conflicting with actual pointer storage functions. + - 2015/01/11 (1.30) - big font/image API change! now loads TTF file. allow for multiple fonts. no need for a PNG loader. + (1.30) - removed GetDefaultFontData(). uses io.Fonts->GetTextureData*() API to retrieve uncompressed pixels. + this sequence: + const void* png_data; + unsigned int png_size; + ImGui::GetDefaultFontData(NULL, NULL, &png_data, &png_size); + // + became: + unsigned char* pixels; + int width, height; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); + // + io.Fonts->TexID = (your_texture_identifier); + you now have much more flexibility to load multiple TTF fonts and manage the texture buffer for internal needs. + it is now recommended that you sample the font texture with bilinear interpolation. + (1.30) - added texture identifier in ImDrawCmd passed to your render function (we can now render images). make sure to set io.Fonts->TexID. + (1.30) - removed IO.PixelCenterOffset (unnecessary, can be handled in user projection matrix) + (1.30) - removed ImGui::IsItemFocused() in favor of ImGui::IsItemActive() which handles all widgets + - 2014/12/10 (1.18) - removed SetNewWindowDefaultPos() in favor of new generic API SetNextWindowPos(pos, ImGuiSetCondition_FirstUseEver) + - 2014/11/28 (1.17) - moved IO.Font*** options to inside the IO.Font-> structure (FontYOffset, FontTexUvForWhite, FontBaseScale, FontFallbackGlyph) + - 2014/11/26 (1.17) - reworked syntax of IMGUI_ONCE_UPON_A_FRAME helper macro to increase compiler compatibility + - 2014/11/07 (1.15) - renamed IsHovered() to IsItemHovered() + - 2014/10/02 (1.14) - renamed IMGUI_INCLUDE_IMGUI_USER_CPP to IMGUI_INCLUDE_IMGUI_USER_INL and imgui_user.cpp to imgui_user.inl (more IDE friendly) + - 2014/09/25 (1.13) - removed 'text_end' parameter from IO.SetClipboardTextFn (the string is now always zero-terminated for simplicity) + - 2014/09/24 (1.12) - renamed SetFontScale() to SetWindowFontScale() + - 2014/09/24 (1.12) - moved IM_MALLOC/IM_REALLOC/IM_FREE preprocessor defines to IO.MemAllocFn/IO.MemReallocFn/IO.MemFreeFn + - 2014/08/30 (1.09) - removed IO.FontHeight (now computed automatically) + - 2014/08/30 (1.09) - moved IMGUI_FONT_TEX_UV_FOR_WHITE preprocessor define to IO.FontTexUvForWhite + - 2014/08/28 (1.09) - changed the behavior of IO.PixelCenterOffset following various rendering fixes + + + ISSUES & TODO-LIST + ================== + See TODO.txt + + + FREQUENTLY ASKED QUESTIONS (FAQ), TIPS + ====================================== + + Q: How can I help? + A: - If you are experienced with Dear ImGui and C++, look at the github issues, or TODO.txt and see how you want/can help! + - Convince your company to fund development time! Individual users: you can also become a Patron (patreon.com/imgui) or donate on PayPal! See README. + - Disclose your usage of dear imgui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. + You may post screenshot or links in the gallery threads (github.com/ocornut/imgui/issues/1269). Visuals are ideal as they inspire other programmers. + But even without visuals, disclosing your use of dear imgui help the library grow credibility, and help other teams and programmers with taking decisions. + - If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues (on github or privately). + + Q: How can I display an image? What is ImTextureID, how does it works? + A: ImTextureID is a void* used to pass renderer-agnostic texture references around until it hits your render function. + Dear ImGui knows nothing about what those bits represent, it just passes them around. It is up to you to decide what you want the void* to carry! + It could be an identifier to your OpenGL texture (cast GLuint to void*), a pointer to your custom engine material (cast MyMaterial* to void*), etc. + At the end of the chain, your renderer takes this void* to cast it back into whatever it needs to select a current texture to render. + Refer to examples applications, where each renderer (in a imgui_impl_xxxx.cpp file) is treating ImTextureID as a different thing. + (c++ tip: OpenGL uses integers to identify textures. You can safely store an integer into a void*, just cast it to void*, don't take it's address!) + To display a custom image/texture within an ImGui window, you may use ImGui::Image(), ImGui::ImageButton(), ImDrawList::AddImage() functions. + Dear ImGui will generate the geometry and draw calls using the ImTextureID that you passed and which your renderer can use. + You may call ImGui::ShowMetricsWindow() to explore active draw lists and visualize/understand how the draw data is generated. + It is your responsibility to get textures uploaded to your GPU. + + Q: Can I have multiple widgets with the same label? Can I have widget without a label? + A: Yes. A primer on labels and the ID stack... + + - Elements that are typically not clickable, such as Text() items don't need an ID. + + - Interactive widgets require state to be carried over multiple frames (most typically Dear ImGui often needs to remember what is + the "active" widget). to do so they need a unique ID. unique ID are typically derived from a string label, an integer index or a pointer. + + Button("OK"); // Label = "OK", ID = hash of "OK" + Button("Cancel"); // Label = "Cancel", ID = hash of "Cancel" + + - ID are uniquely scoped within windows, tree nodes, etc. so no conflict can happen if you have two buttons called "OK" + in two different windows or in two different locations of a tree. + + - If you have a same ID twice in the same location, you'll have a conflict: + + Button("OK"); + Button("OK"); // ID collision! Both buttons will be treated as the same. + + Fear not! this is easy to solve and there are many ways to solve it! + + - When passing a label you can optionally specify extra unique ID information within string itself. + Use "##" to pass a complement to the ID that won't be visible to the end-user. + This helps solving the simple collision cases when you know which items are going to be created. + + Button("Play"); // Label = "Play", ID = hash of "Play" + Button("Play##foo1"); // Label = "Play", ID = hash of "Play##foo1" (different from above) + Button("Play##foo2"); // Label = "Play", ID = hash of "Play##foo2" (different from above) + + - If you want to completely hide the label, but still need an ID: + + Checkbox("##On", &b); // Label = "", ID = hash of "##On" (no label!) + + - Occasionally/rarely you might want change a label while preserving a constant ID. This allows you to animate labels. + For example you may want to include varying information in a window title bar, but windows are uniquely identified by their ID.. + Use "###" to pass a label that isn't part of ID: + + Button("Hello###ID"; // Label = "Hello", ID = hash of "ID" + Button("World###ID"; // Label = "World", ID = hash of "ID" (same as above) + + sprintf(buf, "My game (%f FPS)###MyGame", fps); + Begin(buf); // Variable label, ID = hash of "MyGame" + + - Use PushID() / PopID() to create scopes and avoid ID conflicts within the same Window. + This is the most convenient way of distinguishing ID if you are iterating and creating many UI elements. + You can push a pointer, a string or an integer value. Remember that ID are formed from the concatenation of _everything_ in the ID stack! + + for (int i = 0; i < 100; i++) + { + PushID(i); + Button("Click"); // Label = "Click", ID = hash of integer + "label" (unique) + PopID(); + } + + for (int i = 0; i < 100; i++) + { + MyObject* obj = Objects[i]; + PushID(obj); + Button("Click"); // Label = "Click", ID = hash of pointer + "label" (unique) + PopID(); + } + + for (int i = 0; i < 100; i++) + { + MyObject* obj = Objects[i]; + PushID(obj->Name); + Button("Click"); // Label = "Click", ID = hash of string + "label" (unique) + PopID(); + } + + - More example showing that you can stack multiple prefixes into the ID stack: + + Button("Click"); // Label = "Click", ID = hash of "Click" + PushID("node"); + Button("Click"); // Label = "Click", ID = hash of "node" + "Click" + PushID(my_ptr); + Button("Click"); // Label = "Click", ID = hash of "node" + ptr + "Click" + PopID(); + PopID(); + + - Tree nodes implicitly creates a scope for you by calling PushID(). + + Button("Click"); // Label = "Click", ID = hash of "Click" + if (TreeNode("node")) + { + Button("Click"); // Label = "Click", ID = hash of "node" + "Click" + TreePop(); + } + + - When working with trees, ID are used to preserve the open/close state of each tree node. + Depending on your use cases you may want to use strings, indices or pointers as ID. + e.g. when displaying a single object that may change over time (dynamic 1-1 relationship), using a static string as ID will preserve your + node open/closed state when the targeted object change. + e.g. when displaying a list of objects, using indices or pointers as ID will preserve the node open/closed state differently. + experiment and see what makes more sense! + + Q: How can I tell when Dear ImGui wants my mouse/keyboard inputs VS when I can pass them to my application? + A: You can read the 'io.WantCaptureMouse'/'io.WantCaptureKeyboard'/'ioWantTextInput' flags from the ImGuiIO structure. + - When 'io.WantCaptureMouse' or 'io.WantCaptureKeyboard' flags are set you may want to discard/hide the inputs from the rest of your application. + - When 'io.WantTextInput' is set to may want to notify your OS to popup an on-screen keyboard, if available (e.g. on a mobile phone, or console OS). + Preferably read the flags after calling ImGui::NewFrame() to avoid them lagging by one frame. But reading those flags before calling NewFrame() is + also generally ok, as the bool toggles fairly rarely and you don't generally expect to interact with either Dear ImGui or your application during + the same frame when that transition occurs. Dear ImGui is tracking dragging and widget activity that may occur outside the boundary of a window, + so 'io.WantCaptureMouse' is more accurate and correct than checking if a window is hovered. + (Advanced note: text input releases focus on Return 'KeyDown', so the following Return 'KeyUp' event that your application receive will typically + have 'io.WantCaptureKeyboard=false'. Depending on your application logic it may or not be inconvenient. You might want to track which key-downs + were for Dear ImGui, e.g. with an array of bool, and filter out the corresponding key-ups.) + + Q: How can I load a different font than the default? (default is an embedded version of ProggyClean.ttf, rendered at size 13) + A: Use the font atlas to load the TTF/OTF file you want: + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); + io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() + + New programmers: remember that in C/C++ and most programming languages if you want to use a backslash \ in a string literal you need to write a double backslash "\\": + io.Fonts->AddFontFromFileTTF("MyDataFolder\MyFontFile.ttf", size_in_pixels); // WRONG + io.Fonts->AddFontFromFileTTF("MyDataFolder\\MyFontFile.ttf", size_in_pixels); // CORRECT + io.Fonts->AddFontFromFileTTF("MyDataFolder/MyFontFile.ttf", size_in_pixels); // ALSO CORRECT + + Q: How can I easily use icons in my application? + A: The most convenient and practical way is to merge an icon font such as FontAwesome inside you main font. Then you can refer to icons within your + strings. Read 'How can I load multiple fonts?' and the file 'misc/fonts/README.txt' for instructions and useful header files. + + Q: How can I load multiple fonts? + A: Use the font atlas to pack them into a single texture: + (Read misc/fonts/README.txt and the code in ImFontAtlas for more details.) + + ImGuiIO& io = ImGui::GetIO(); + ImFont* font0 = io.Fonts->AddFontDefault(); + ImFont* font1 = io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); + ImFont* font2 = io.Fonts->AddFontFromFileTTF("myfontfile2.ttf", size_in_pixels); + io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() + // the first loaded font gets used by default + // use ImGui::PushFont()/ImGui::PopFont() to change the font at runtime + + // Options + ImFontConfig config; + config.OversampleH = 3; + config.OversampleV = 1; + config.GlyphOffset.y -= 2.0f; // Move everything by 2 pixels up + config.GlyphExtraSpacing.x = 1.0f; // Increase spacing between characters + io.Fonts->LoadFromFileTTF("myfontfile.ttf", size_pixels, &config); + + // Combine multiple fonts into one (e.g. for icon fonts) + ImWchar ranges[] = { 0xf000, 0xf3ff, 0 }; + ImFontConfig config; + config.MergeMode = true; + io.Fonts->AddFontDefault(); + io.Fonts->LoadFromFileTTF("fontawesome-webfont.ttf", 16.0f, &config, ranges); // Merge icon font + io.Fonts->LoadFromFileTTF("myfontfile.ttf", size_pixels, NULL, &config, io.Fonts->GetGlyphRangesJapanese()); // Merge japanese glyphs + + Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? + A: When loading a font, pass custom Unicode ranges to specify the glyphs to load. + + // Add default Japanese ranges + io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, io.Fonts->GetGlyphRangesJapanese()); + + // Or create your own custom ranges (e.g. for a game you can feed your entire game script and only build the characters the game need) + ImVector ranges; + ImFontAtlas::GlyphRangesBuilder builder; + builder.AddText("Hello world"); // Add a string (here "Hello world" contains 7 unique characters) + builder.AddChar(0x7262); // Add a specific character + builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); // Add one of the default ranges + builder.BuildRanges(&ranges); // Build the final result (ordered ranges with all the unique characters submitted) + io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, ranges.Data); + + All your strings needs to use UTF-8 encoding. In C++11 you can encode a string literal in UTF-8 by using the u8"hello" syntax. + Specifying literal in your source code using a local code page (such as CP-923 for Japanese or CP-1251 for Cyrillic) will NOT work! + Otherwise you can convert yourself to UTF-8 or load text data from file already saved as UTF-8. + + Text input: it is up to your application to pass the right character code to io.AddInputCharacter(). The applications in examples/ are doing that. + For languages using IME, on Windows you can copy the Hwnd of your application to io.ImeWindowHandle. + The default implementation of io.ImeSetInputScreenPosFn() on Windows will set your IME position correctly. + + Q: How can I preserve my Dear ImGui context across reloading a DLL? (loss of the global/static variables) + A: Create your own context 'ctx = CreateContext()' + 'SetCurrentContext(ctx)' and your own font atlas 'ctx->GetIO().Fonts = new ImFontAtlas()' + so you don't rely on the default globals. + + Q: How can I use the drawing facilities without an ImGui window? (using ImDrawList API) + A: - You can create a dummy window. Call Begin() with NoTitleBar|NoResize|NoMove|NoScrollbar|NoSavedSettings|NoInputs flag, + push a ImGuiCol_WindowBg with zero alpha, then retrieve the ImDrawList* via GetWindowDrawList() and draw to it in any way you like. + - You can call ImGui::GetOverlayDrawList() and use this draw list to display contents over every other imgui windows. + - You can create your own ImDrawList instance. You'll need to initialize them ImGui::GetDrawListSharedData(), or create your own ImDrawListSharedData. + + Q: I integrated Dear ImGui in my engine and the text or lines are blurry.. + A: In your Render function, try translating your projection matrix by (0.5f,0.5f) or (0.375f,0.375f). + Also make sure your orthographic projection matrix and io.DisplaySize matches your actual framebuffer dimension. + + Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around.. + A: You are probably mishandling the clipping rectangles in your render function. + Rectangles provided by ImGui are defined as (x1=left,y1=top,x2=right,y2=bottom) and NOT as (x1,y1,width,height). + + + - tip: you can call Begin() multiple times with the same name during the same frame, it will keep appending to the same window. + this is also useful to set yourself in the context of another window (to get/set other settings) + - tip: you can create widgets without a Begin()/End() block, they will go in an implicit window called "Debug". + - tip: the ImGuiOnceUponAFrame helper will allow run the block of code only once a frame. You can use it to quickly add custom UI in the middle + of a deep nested inner loop in your code. + - tip: you can call Render() multiple times (e.g for VR renders). + - tip: call and read the ShowDemoWindow() code in imgui_demo.cpp for more example of how to use ImGui! + +*/ + +#if defined(_MSC_VER) && !defined(_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 +#else +#include // intptr_t +#endif + +#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 +#endif + +// Clang warnings with -Weverything +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning : unknown warning group '-Wformat-pedantic *' // not all warnings are known by all clang versions.. so ignoring warnings triggers new warnings on some configuration. great! +#pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning : format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning : declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning : declaration requires a global destructor // similar to above, not sure what the exact difference it. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // +#pragma clang diagnostic ignored "-Wformat-pedantic" // warning : format specifies type 'void *' but the argument has type 'xxxx *' // unreasonable, would lead to casting every %p arg to void*. probably enabled by -pedantic. +#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning : cast to 'void *' from smaller integer type 'int' // +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'void*', but argument 6 has type 'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'xxxx' to type 'xxxx' casts away qualifiers +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when assuming that (X - c) > X is always false +#endif + +// Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall +#ifdef _MSC_VER +#define IMGUI_CDECL __cdecl +#else +#define IMGUI_CDECL +#endif + +//------------------------------------------------------------------------- +// Forward Declarations +//------------------------------------------------------------------------- + +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 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 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 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 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 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); +} + +//----------------------------------------------------------------------------- +// 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); + +//----------------------------------------------------------------------------- +// 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. +// 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; +#endif + +// Memory Allocator functions. Use SetAllocatorFunctions() to change them. +// If you use DLL hotreloading you might need to call SetAllocatorFunctions() after reloading code from this file. +// Otherwise, you probably don't want to modify them mid-program, and if you use global/static e.g. ImVector<> instances you may need to keep them accessible during program destruction. +#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); } +#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); } +#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; + +//----------------------------------------------------------------------------- +// User facing structures +//----------------------------------------------------------------------------- + +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); +} + +// 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); +} + +ImGuiIO::ImGuiIO() +{ + // Most fields are initialized with zero + memset(this, 0, sizeof(*this)); + + // Settings + DisplaySize = ImVec2(-1.0f, -1.0f); + DeltaTime = 1.0f/60.0f; + NavFlags = 0x00; + IniSavingRate = 5.0f; + IniFilename = "imgui.ini"; + LogFilename = "imgui_log.txt"; + MouseDoubleClickTime = 0.30f; + MouseDoubleClickMaxDist = 6.0f; + for (int i = 0; i < ImGuiKey_COUNT; i++) + KeyMap[i] = -1; + KeyRepeatDelay = 0.250f; + KeyRepeatRate = 0.050f; + UserData = NULL; + + Fonts = NULL; + FontGlobalScale = 1.0f; + FontDefault = NULL; + FontAllowUserScaling = false; + DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + DisplayVisibleMin = DisplayVisibleMax = ImVec2(0.0f, 0.0f); + + // Advanced/subtle behaviors +#ifdef __APPLE__ + OptMacOSXBehaviors = true; // Set Mac OS X style defaults based on __APPLE__ compile time flag +#else + OptMacOSXBehaviors = false; +#endif + OptCursorBlink = true; + + // 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; +#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; +} + +// Pass in translated ASCII characters for text input. +// - with glfw you can get those from the callback set in glfwSetCharCallback() +// - on Windows you can get those using ToAscii+keyboard state, or via the WM_CHAR message +void ImGuiIO::AddInputCharacter(ImWchar c) +{ + 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) +{ + // 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 + +// 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" +#else +#define IM_NEWLINE "\n" +#endif + +ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p) +{ + ImVec2 ap = p - a; + ImVec2 ab_dir = b - a; + float dot = ap.x * ab_dir.x + ap.y * ab_dir.y; + if (dot < 0.0f) + return a; + float ab_len_sqr = ab_dir.x * ab_dir.x + ab_dir.y * ab_dir.y; + if (dot > ab_len_sqr) + return b; + return a + ab_dir * dot / ab_len_sqr; +} + +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)); +} + +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 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; +} + +int ImStricmp(const char* str1, const char* str2) +{ + 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 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) +{ + if (count < 1) return; + strncpy(dst, src, count); + dst[count-1] = 0; +} + +char* ImStrdup(const char *str) +{ + 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) +{ + for ( ; str < str_end; str++) + if (*str == c) + return (char*)str; + return NULL; +} + +int ImStrlenW(const ImWchar* str) +{ + int n = 0; + while (*str++) n++; + return n; +} + +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; +} + +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); + + 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) +{ + 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). +// Ideally we would test for only one of those limits at runtime depending on the behavior the vsnprintf(), but trying to deduct it at compile time sounds like a pandora can of worm. +// B) When buf==NULL vsnprintf() will return the output size. +#ifndef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS +int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + int w = vsnprintf(buf, buf_size, fmt, args); + va_end(args); + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; +} + +int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) +{ + int w = vsnprintf(buf, buf_size, fmt, args); + if (buf == NULL) + return w; + if (w == -1 || w >= (int)buf_size) + w = (int)buf_size - 1; + buf[w] = 0; + return w; +} +#endif // #ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS + +// Pass data_size==0 for zero-terminated strings +// FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. +ImU32 ImHash(const void* data, int data_size, ImU32 seed) +{ + static ImU32 crc32_lut[256] = { 0 }; + if (!crc32_lut[1]) + { + const ImU32 polynomial = 0xEDB88320; + for (ImU32 i = 0; i < 256; i++) + { + ImU32 crc = i; + for (ImU32 j = 0; j < 8; j++) + crc = (crc >> 1) ^ (ImU32(-int(crc & 1)) & polynomial); + crc32_lut[i] = crc; + } + } + + seed = ~seed; + ImU32 crc = seed; + const unsigned char* current = (const unsigned char*)data; + + if (data_size > 0) + { + // Known size + while (data_size--) + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ *current++]; + } + else + { + // Zero-terminated string + while (unsigned char c = *current++) + { + // We support a syntax of "label###id" where only "###id" is included in the hash, and only "label" gets displayed. + // Because this syntax is rarely used we are optimizing for the common case. + // - If we reach ### in the string we discard the hash so far and reset to the seed. + // - We don't do 'current += 2; continue;' after handling ### to keep the code smaller. + if (c == '#' && current[0] == '#' && current[1] == '#') + crc = seed; + crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; + } + } + return ~crc; +} + +//----------------------------------------------------------------------------- +// ImText* helpers +//----------------------------------------------------------------------------- + +// 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; +} + +// 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 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; +} + +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; +} + +ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) +{ + 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); +} + +ImU32 ImGui::ColorConvertFloat4ToU32(const ImVec4& in) +{ + 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); +} + +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); +} + +// 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; +} + +// Convert hsv floats ([0-1],[0-1],[0-1]) to rgb floats ([0-1],[0-1],[0-1]), from Foley & van Dam p593 +// also http://en.wikipedia.org/wiki/HSL_and_HSV +void ImGui::ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b) +{ + if (s == 0.0f) + { + // gray + out_r = out_g = out_b = v; + return; + } + + h = fmodf(h, 1.0f) / (60.0f/360.0f); + int i = (int)h; + float f = h - (float)i; + float p = v * (1.0f - s); + float q = v * (1.0f - s * f); + float t = v * (1.0f - s * (1.0f - f)); + + switch (i) + { + case 0: out_r = v; out_g = t; out_b = p; break; + case 1: out_r = q; out_g = v; out_b = p; break; + case 2: out_r = p; out_g = v; out_b = t; break; + case 3: out_r = p; out_g = q; out_b = v; break; + case 4: out_r = t; out_g = p; out_b = v; break; + case 5: default: out_r = v; out_g = p; out_b = q; break; + } +} + +FILE* ImFileOpen(const char* filename, const char* mode) +{ +#if defined(_WIN32) && !defined(__CYGWIN__) + // We need a fopen() wrapper because MSVC/Windows fopen doesn't handle UTF-8 filenames. Converting both strings from UTF-8 to wchar format (using a single allocation, because we can) + const int filename_wsize = ImTextCountCharsFromUtf8(filename, NULL) + 1; + const int mode_wsize = ImTextCountCharsFromUtf8(mode, NULL) + 1; + ImVector buf; + buf.resize(filename_wsize + mode_wsize); + ImTextStrFromUtf8(&buf[0], filename_wsize, filename, NULL); + ImTextStrFromUtf8(&buf[filename_wsize], mode_wsize, mode, NULL); + return _wfopen((wchar_t*)&buf[0], (wchar_t*)&buf[filename_wsize]); +#else + return fopen(filename, mode); +#endif +} + +// 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; +} + +//----------------------------------------------------------------------------- +// ImGuiStorage +// Helper: Key->value storage +//----------------------------------------------------------------------------- + +// 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; +} + +// 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); +} + +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; +} + +bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const +{ + 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; +} + +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; +} + +// 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) +{ + 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) +{ + return (bool*)GetIntRef(key, default_val ? 1 : 0); +} + +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; +} + +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; +} + +// 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; +} + +void ImGuiStorage::SetBool(ImGuiID key, bool val) +{ + 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; +} + +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; +} + +void ImGuiStorage::SetAllInt(int v) +{ + for (int i = 0; i < Data.Size; i++) + Data[i].val_i = v; +} + +//----------------------------------------------------------------------------- +// ImGuiTextFilter +//----------------------------------------------------------------------------- + +// 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)); +} + +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; +} + +//----------------------------------------------------------------------------- +// ImGuiTextBuffer +//----------------------------------------------------------------------------- + +// 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) +#endif + +// Helper: Text buffer for logging/accumulating text +void ImGuiTextBuffer::appendfv(const char* fmt, va_list 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; + + 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); +} + +void ImGuiTextBuffer::appendf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + appendfv(fmt, args); + va_end(args); +} + +//----------------------------------------------------------------------------- +// ImGuiSimpleColumns (internal use only) +//----------------------------------------------------------------------------- + +ImGuiMenuColumns::ImGuiMenuColumns() +{ + 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); +} + +float ImGuiMenuColumns::CalcExtraSpace(float avail_w) +{ + return ImMax(0.0f, avail_w - Width); +} + +//----------------------------------------------------------------------------- +// ImGuiListClipper +//----------------------------------------------------------------------------- + +static void SetCursorPosYAndSetupDummyPrevLine(float pos_y, float line_height) +{ + // Set cursor position and a few other things so that SetScrollHere() and Columns() can work when seeking cursor. + // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue. Consider moving within SetCursorXXX functions? + ImGui::SetCursorPosY(pos_y); + ImGuiWindow* window = ImGui::GetCurrentWindow(); + window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height; // Setting those fields so that SetScrollHere() can properly function after the end of our clipper usage. + window->DC.PrevLineHeight = (line_height - GImGui->Style.ItemSpacing.y); // If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list. + if (window->DC.ColumnsSet) + window->DC.ColumnsSet->CellMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly +} + +// Use case A: Begin() called from constructor with items_height<0, then called again from Sync() in StepNo 1 +// Use case B: Begin() called from constructor with items_height>0 +// FIXME-LEGACY: Ideally we should remove the Begin/End functions but they are part of the legacy API we still support. This is why some of the code in Step() calling Begin() and reassign some fields, spaghetti style. +void ImGuiListClipper::Begin(int count, float items_height) +{ + StartPosY = ImGui::GetCursorPosY(); + ItemsHeight = items_height; + ItemsCount = count; + StepNo = 0; + DisplayEnd = DisplayStart = -1; + if (ItemsHeight > 0.0f) + { + ImGui::CalcListClipping(ItemsCount, ItemsHeight, &DisplayStart, &DisplayEnd); // calculate how many to clip/display + if (DisplayStart > 0) + SetCursorPosYAndSetupDummyPrevLine(StartPosY + DisplayStart * ItemsHeight, ItemsHeight); // advance cursor + StepNo = 2; + } +} + +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; +} + +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; +} + +//----------------------------------------------------------------------------- +// 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() +{ + 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 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 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 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 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) +{ + 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; +} + +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; +} + +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; + } +} + +ImGuiID ImGui::GetActiveID() +{ + ImGuiContext& g = *GImGui; + return g.ActiveId; +} + +void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) +{ + 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); + + if (g.ActiveIdSource == ImGuiInputSource_Nav) + g.NavDisableMouseHover = true; + else + g.NavDisableHighlight = true; +} + +void ImGui::ClearActiveID() +{ + 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; +} + +ImGuiID ImGui::GetHoveredID() +{ + 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; +} + +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; + } + + return true; +} + +// Advance cursor given item size for layout. +void ImGui::ItemSize(const ImVec2& size, float text_offset_y) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + // Always align ourselves on pixel boundaries + const float line_height = ImMax(window->DC.CurrentLineHeight, size.y); + const float text_base_offset = ImMax(window->DC.CurrentLineTextBaseOffset, text_offset_y); + //if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] + window->DC.CursorPosPrevLine = ImVec2(window->DC.CursorPos.x + size.x, window->DC.CursorPos.y); + window->DC.CursorPos = ImVec2((float)(int)(window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX), (float)(int)(window->DC.CursorPos.y + line_height + g.Style.ItemSpacing.y)); + window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); + //if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] + + window->DC.PrevLineHeight = line_height; + window->DC.PrevLineTextBaseOffset = text_base_offset; + window->DC.CurrentLineHeight = window->DC.CurrentLineTextBaseOffset = 0.0f; + + // Horizontal layout mode + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + SameLine(); +} + +void ImGui::ItemSize(const ImRect& bb, float text_offset_y) +{ + ItemSize(bb.GetSize(), text_offset_y); +} + +static ImGuiDir NavScoreItemGetQuadrant(float dx, float dy) +{ + if (fabsf(dx) > fabsf(dy)) + return (dx > 0.0f) ? ImGuiDir_Right : ImGuiDir_Left; + return (dy > 0.0f) ? ImGuiDir_Down : ImGuiDir_Up; +} + +static float NavScoreItemDistInterval(float a0, float a1, float b0, float b1) +{ + if (a1 < b0) + return a1 - b0; + if (b1 < a0) + return a0 - b1; + return 0.0f; +} + +// Scoring function for directional navigation. Based on https://gist.github.com/rygorous/6981057 +static bool NavScoreItem(ImGuiNavMoveResult* result, ImRect cand) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavLayer != window->DC.NavLayerCurrent) + return false; + + const ImRect& curr = g.NavScoringRectScreen; // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width) + g.NavScoringCount++; + + // We perform scoring on items bounding box clipped by their parent window on the other axis (clipping on our movement axis would give us equal scores for all clipped items) + if (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) + { + cand.Min.y = ImClamp(cand.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y); + cand.Max.y = ImClamp(cand.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y); + } + else + { + cand.Min.x = ImClamp(cand.Min.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + cand.Max.x = ImClamp(cand.Max.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + } + + // Compute distance between boxes + // FIXME-NAV: Introducing biases for vertical navigation, needs to be removed. + float dbx = NavScoreItemDistInterval(cand.Min.x, cand.Max.x, curr.Min.x, curr.Max.x); + float dby = NavScoreItemDistInterval(ImLerp(cand.Min.y, cand.Max.y, 0.2f), ImLerp(cand.Min.y, cand.Max.y, 0.8f), ImLerp(curr.Min.y, curr.Max.y, 0.2f), ImLerp(curr.Min.y, curr.Max.y, 0.8f)); // Scale down on Y to keep using box-distance for vertically touching items + if (dby != 0.0f && dbx != 0.0f) + dbx = (dbx/1000.0f) + ((dbx > 0.0f) ? +1.0f : -1.0f); + float dist_box = fabsf(dbx) + fabsf(dby); + + // Compute distance between centers (this is off by a factor of 2, but we only compare center distances with each other so it doesn't matter) + float dcx = (cand.Min.x + cand.Max.x) - (curr.Min.x + curr.Max.x); + float dcy = (cand.Min.y + cand.Max.y) - (curr.Min.y + curr.Max.y); + float dist_center = fabsf(dcx) + fabsf(dcy); // L1 metric (need this for our connectedness guarantee) + + // Determine which quadrant of 'curr' our candidate item 'cand' lies in based on distance + ImGuiDir quadrant; + float dax = 0.0f, day = 0.0f, dist_axial = 0.0f; + if (dbx != 0.0f || dby != 0.0f) + { + // For non-overlapping boxes, use distance between boxes + dax = dbx; + day = dby; + dist_axial = dist_box; + quadrant = NavScoreItemGetQuadrant(dbx, dby); + } + else if (dcx != 0.0f || dcy != 0.0f) + { + // For overlapping boxes with different centers, use distance between centers + dax = dcx; + day = dcy; + dist_axial = dist_center; + quadrant = NavScoreItemGetQuadrant(dcx, dcy); + } + else + { + // Degenerate case: two overlapping buttons with same center, break ties arbitrarily (note that LastItemId here is really the _previous_ item order, but it doesn't matter) + quadrant = (window->DC.LastItemId < g.NavId) ? ImGuiDir_Left : ImGuiDir_Right; + } + +#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; +} + +static void NavSaveLastChildNavWindow(ImGuiWindow* child_window) +{ + ImGuiWindow* parent_window = child_window; + while (parent_window && (parent_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 && (parent_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + parent_window = parent_window->ParentWindow; + if (parent_window && parent_window != child_window) + parent_window->NavLastChildNavWindow = child_window; +} + +// Call when we are expected to land on Layer 0 after FocusWindow() +static ImGuiWindow* NavRestoreLastChildNavWindow(ImGuiWindow* window) +{ + return window->NavLastChildNavWindow ? window->NavLastChildNavWindow : window; +} + +static void NavRestoreLayer(int layer) +{ + ImGuiContext& g = *GImGui; + g.NavLayer = layer; + if (layer == 0) + g.NavWindow = NavRestoreLastChildNavWindow(g.NavWindow); + if (layer == 0 && g.NavWindow->NavLastIds[0] != 0) + SetNavIDAndMoveMouse(g.NavWindow->NavLastIds[0], layer, g.NavWindow->NavRectRel[0]); + else + ImGui::NavInitWindow(g.NavWindow, true); +} + +static inline void NavUpdateAnyRequestFlag() +{ + ImGuiContext& g = *GImGui; + g.NavAnyRequest = g.NavMoveRequest || g.NavInitRequest || IMGUI_DEBUG_NAV_SCORING; +} + +static bool NavMoveRequestButNoResultYet() +{ + ImGuiContext& g = *GImGui; + return g.NavMoveRequest && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; +} + +void ImGui::NavMoveRequestCancel() +{ + 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; +#if IMGUI_DEBUG_NAV_SCORING + // [DEBUG] Score all items in NavWindow at all times + if (!g.NavMoveRequest) + g.NavMoveDir = g.NavMoveDirLast; + bool new_best = NavScoreItem(result, nav_bb) && g.NavMoveRequest; +#else + bool new_best = g.NavMoveRequest && NavScoreItem(result, nav_bb); +#endif + if (new_best) + { + result->ID = id; + result->ParentID = window->IDStack.back(); + result->Window = window; + result->RectRel = nav_bb_rel; + } + } + + // Update window-relative bounding box of navigated item + if (g.NavId == id) + { + g.NavWindow = window; // Always refresh g.NavWindow, because some operations such as FocusItem() don't have a window. + g.NavLayer = window->DC.NavLayerCurrent; + g.NavIdIsAlive = true; + g.NavIdTabCounter = window->FocusIdxTabCounter; + window->NavRectRel[window->DC.NavLayerCurrent] = nav_bb_rel; // Store item bounding box (relative to window position) + } +} + +// 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; +} + +// This is roughly matching the behavior of internal-facing ItemHoverable() +// - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() +// - this should work even for non-interactive items that have no ID, so we cannot use LastItemId +bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + 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) +{ + 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; + + SetHoveredID(id); + return true; +} + +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; +} + +bool ImGui::FocusableItemRegister(ImGuiWindow* window, ImGuiID id, bool tab_stop) +{ + 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++; + + // 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; + } + + return false; +} + +void ImGui::FocusableItemUnregister(ImGuiWindow* window) +{ + 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; +} + +float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) +{ + 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 + + return ImMax(wrap_pos_x - pos.x, 1.0f); +} + +//----------------------------------------------------------------------------- + +void* ImGui::MemAlloc(size_t sz) +{ + GImAllocatorActiveAllocationsCount++; + return GImAllocatorAllocFunc(sz, GImAllocatorUserData); +} + +void ImGui::MemFree(void* ptr) +{ + if (ptr) GImAllocatorActiveAllocationsCount--; + return GImAllocatorFreeFunc(ptr, GImAllocatorUserData); +} + +const char* ImGui::GetClipboardText() +{ + return GImGui->IO.GetClipboardTextFn ? GImGui->IO.GetClipboardTextFn(GImGui->IO.ClipboardUserData) : ""; +} + +void ImGui::SetClipboardText(const char* text) +{ + if (GImGui->IO.SetClipboardTextFn) + GImGui->IO.SetClipboardTextFn(GImGui->IO.ClipboardUserData, text); +} + +const char* ImGui::GetVersion() +{ + 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() +{ + return GImGui; +} + +void ImGui::SetCurrentContext(ImGuiContext* ctx) +{ +#ifdef IMGUI_SET_CURRENT_CONTEXT_FUNC + IMGUI_SET_CURRENT_CONTEXT_FUNC(ctx); // For custom thread-based hackery you may want to have control over this. +#else + GImGui = ctx; +#endif +} + +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; +} + +ImGuiContext* ImGui::CreateContext(ImFontAtlas* shared_font_atlas) +{ + ImGuiContext* ctx = IM_NEW(ImGuiContext)(shared_font_atlas); + if (GImGui == NULL) + SetCurrentContext(ctx); + Initialize(ctx); + return ctx; +} + +void ImGui::DestroyContext(ImGuiContext* ctx) +{ + if (ctx == NULL) + ctx = GImGui; + Shutdown(ctx); + if (GImGui == ctx) + SetCurrentContext(NULL); + IM_DELETE(ctx); +} + +ImGuiIO& ImGui::GetIO() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() or ImGui::SetCurrentContext()?"); + return GImGui->IO; +} + +ImGuiStyle& ImGui::GetStyle() +{ + 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() +{ + ImGuiContext& g = *GImGui; + return g.DrawData.Valid ? &g.DrawData : NULL; +} + +float ImGui::GetTime() +{ + return GImGui->Time; +} + +int ImGui::GetFrameCount() +{ + return GImGui->FrameCount; +} + +ImDrawList* ImGui::GetOverlayDrawList() +{ + return &GImGui->OverlayDrawList; +} + +ImDrawListSharedData* ImGui::GetDrawListSharedData() +{ + 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]; + } +} + +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. +} + +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; +} + +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; +} + +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; +} + +// Equivalent of IsKeyDown() for NavInputs[] +static bool IsNavInputDown(ImGuiNavInput n) +{ + 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; +} + +static bool IsNavInputPressedAnyOfTwo(ImGuiNavInput n1, ImGuiNavInput n2, ImGuiInputReadMode mode) +{ + 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; +} + +static void NavUpdateWindowingHighlightWindow(int focus_change_dir) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindowingTarget); + if (g.NavWindowingTarget->Flags & ImGuiWindowFlags_Modal) + return; + + const int i_current = FindWindowIndex(g.NavWindowingTarget); + ImGuiWindow* window_target = FindWindowNavigable(i_current + focus_change_dir, -INT_MAX, focus_change_dir); + if (!window_target) + window_target = FindWindowNavigable((focus_change_dir < 0) ? (g.Windows.Size - 1) : 0, i_current, focus_change_dir); + g.NavWindowingTarget = window_target; + g.NavWindowingToggleLayer = false; +} + +// 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); + } +} + +// 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 ImGui::NavUpdate() +{ + 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; +#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<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; + } +} + +// When using this function it is sane to ensure that float are perfectly rounded to integer values, to that e.g. (int)(max.x-min.x) in user's render produce correct result. +void ImGui::PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DrawList->PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect); + window->ClipRect = window->DrawList->_ClipRectStack.back(); +} + +void ImGui::PopClipRect() +{ + 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; +} + +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() +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + 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* 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; +} + +// Pass text data straight to log (without being displayed) +void ImGui::LogText(const char* fmt, ...) +{ + 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); +} + +// 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; + } +} + +// 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); + } +} + +// 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); +} + +// 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); + } +} + +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); + } +} + +// Render a triangle to denote expanded/collapsed state +void ImGui::RenderTriangle(ImVec2 p_min, ImGuiDir dir, float scale) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const float h = g.FontSize * 1.00f; + float r = h * 0.40f * scale; + ImVec2 center = p_min + ImVec2(h * 0.50f, h * 0.50f * scale); + + ImVec2 a, b, c; + switch (dir) + { + case ImGuiDir_Up: + case ImGuiDir_Down: + if (dir == ImGuiDir_Up) r = -r; + center.y -= r * 0.25f; + a = ImVec2(0,1) * r; + b = ImVec2(-0.866f,-0.5f) * r; + c = ImVec2(+0.866f,-0.5f) * r; + break; + case ImGuiDir_Left: + case ImGuiDir_Right: + if (dir == ImGuiDir_Left) r = -r; + center.x -= r * 0.25f; + a = ImVec2(1,0) * r; + b = ImVec2(-0.500f,+0.866f) * r; + c = ImVec2(-0.500f,-0.866f) * r; + break; + case ImGuiDir_None: + case ImGuiDir_Count_: + IM_ASSERT(0); + break; + } + + window->DrawList->AddTriangleFilled(center + a, center + b, center + c, GetColorU32(ImGuiCol_Text)); +} + +void ImGui::RenderBullet(ImVec2 pos) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DrawList->AddCircleFilled(pos, GImGui->FontSize*0.20f, GetColorU32(ImGuiCol_Text), 8); +} + +void ImGui::RenderCheckMark(ImVec2 pos, ImU32 col, float sz) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + float thickness = ImMax(sz / 5.0f, 1.0f); + sz -= thickness*0.5f; + pos += ImVec2(thickness*0.25f, thickness*0.25f); + + float third = sz / 3.0f; + float bx = pos.x + third; + float by = pos.y + sz - third*0.5f; + window->DrawList->PathLineTo(ImVec2(bx - third, by - third)); + window->DrawList->PathLineTo(ImVec2(bx, by)); + window->DrawList->PathLineTo(ImVec2(bx + third*2, by - third*2)); + window->DrawList->PathStroke(col, false, thickness); +} + +void ImGui::RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags) +{ + ImGuiContext& g = *GImGui; + if (id != g.NavId) + return; + if (g.NavDisableHighlight && !(flags & ImGuiNavHighlightFlags_AlwaysDraw)) + return; + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->DC.NavHideHighlightOneFrame) + return; + + float rounding = (flags & ImGuiNavHighlightFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; + ImRect display_rect = bb; + display_rect.ClipWith(window->ClipRect); + if (flags & ImGuiNavHighlightFlags_TypeDefault) + { + const float THICKNESS = 2.0f; + const float DISTANCE = 3.0f + THICKNESS * 0.5f; + display_rect.Expand(ImVec2(DISTANCE,DISTANCE)); + bool fully_visible = window->ClipRect.Contains(display_rect); + if (!fully_visible) + window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); + window->DrawList->AddRect(display_rect.Min + ImVec2(THICKNESS*0.5f,THICKNESS*0.5f), display_rect.Max - ImVec2(THICKNESS*0.5f,THICKNESS*0.5f), GetColorU32(ImGuiCol_NavHighlight), rounding, ImDrawCornerFlags_All, THICKNESS); + if (!fully_visible) + window->DrawList->PopClipRect(); + } + if (flags & ImGuiNavHighlightFlags_TypeThin) + { + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, ~0, 1.0f); + } +} + +// Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker. +// CalcTextSize("") should return ImVec2(0.0f, GImGui->FontSize) +ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_text_after_double_hash, float wrap_width) +{ + ImGuiContext& g = *GImGui; + + const char* text_display_end; + if (hide_text_after_double_hash) + text_display_end = FindRenderedTextEnd(text, text_end); // Hide anything after a '##' string + else + text_display_end = text_end; + + ImFont* font = g.Font; + const float font_size = g.FontSize; + if (text == text_display_end) + return ImVec2(0.0f, font_size); + ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL); + + // Cancel out character spacing for the last character of a line (it is baked into glyph->AdvanceX field) + const float font_scale = font_size / font->FontSize; + const float character_spacing_x = 1.0f * font_scale; + if (text_size.x > 0.0f) + text_size.x -= character_spacing_x; + text_size.x = (float)(int)(text_size.x + 0.95f); + + return text_size; +} + +// Helper to calculate coarse clipping of large list of evenly sized items. +// NB: Prefer using the ImGuiListClipper higher-level helper if you can! Read comments and instructions there on how those use this sort of pattern. +// NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX +void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.LogEnabled) + { + // If logging is active, do not perform any clipping + *out_items_display_start = 0; + *out_items_display_end = items_count; + return; + } + if (window->SkipItems) + { + *out_items_display_start = *out_items_display_end = 0; + return; + } + + const ImVec2 pos = window->DC.CursorPos; + int start = (int)((window->ClipRect.Min.y - pos.y) / items_height); + int end = (int)((window->ClipRect.Max.y - pos.y) / items_height); + if (g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Up) // When performing a navigation request, ensure we have one item extra in the direction we are moving to + start--; + if (g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) + end++; + + start = ImClamp(start, 0, items_count); + end = ImClamp(end + 1, start, items_count); + *out_items_display_start = start; + *out_items_display_end = end; +} + +// Find window given position, search front-to-back +// FIXME: Note that we have a lag here because WindowRectClipped is updated in Begin() so windows moved by user via SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is called, aka before the next Begin(). Moving window thankfully isn't affected. +static ImGuiWindow* FindHoveredWindow() +{ + ImGuiContext& g = *GImGui; + for (int i = g.Windows.Size - 1; i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + if (!window->Active) + continue; + if (window->Flags & ImGuiWindowFlags_NoInputs) + continue; + + // Using the clipped AABB, a child window will typically be clipped by its parent (not always) + ImRect bb(window->WindowRectClipped.Min - g.Style.TouchExtraPadding, window->WindowRectClipped.Max + g.Style.TouchExtraPadding); + if (bb.Contains(g.IO.MousePos)) + return window; + } + return NULL; +} + +// Test if mouse cursor is hovering given rectangle +// NB- Rectangle is clipped by our current clip setting +// NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) +bool ImGui::IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // 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); +} + +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; +} + +int ImGui::GetKeyIndex(ImGuiKey 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]; +} + +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; +} + +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); +} + +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; +} + +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]; +} + +bool ImGui::IsMouseDown(int 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; +} + +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; + + 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; +} + +bool ImGui::IsMouseReleased(int 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]; +} + +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; +} + +ImVec2 ImGui::GetMousePos() +{ + 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; +} + +// We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position +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; +} + +// 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); +} + +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; +} + +ImGuiMouseCursor ImGui::GetMouseCursor() +{ + return GImGui->MouseCursor; +} + +void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) +{ + GImGui->MouseCursor = cursor_type; +} + +void ImGui::CaptureKeyboardFromApp(bool capture) +{ + GImGui->WantCaptureKeyboardNextFrame = capture ? 1 : 0; +} + +void ImGui::CaptureMouseFromApp(bool capture) +{ + 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; +} + +bool ImGui::IsItemFocused() +{ + 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); +} + +bool ImGui::IsAnyItemHovered() +{ + ImGuiContext& g = *GImGui; + return g.HoveredId != 0 || g.HoveredIdPreviousFrame != 0; +} + +bool ImGui::IsAnyItemActive() +{ + ImGuiContext& g = *GImGui; + return g.ActiveId != 0; +} + +bool ImGui::IsAnyItemFocused() +{ + ImGuiContext& g = *GImGui; + return g.NavId != 0 && !g.NavDisableHighlight; +} + +bool ImGui::IsItemVisible() +{ + 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; +} + +ImVec2 ImGui::GetItemRectMin() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.LastItemRect.Min; +} + +ImVec2 ImGui::GetItemRectMax() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.LastItemRect.Max; +} + +ImVec2 ImGui::GetItemRectSize() +{ + 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); +} + +// 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); +} + +void ImGui::SetTooltipV(const char* fmt, va_list args) +{ + BeginTooltipEx(0, true); + TextV(fmt, args); + EndTooltip(); +} + +void ImGui::SetTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + SetTooltipV(fmt, args); + va_end(args); +} + +void ImGui::BeginTooltip() +{ + BeginTooltipEx(0, false); +} + +void ImGui::EndTooltip() +{ + IM_ASSERT(GetCurrentWindowRead()->Flags & ImGuiWindowFlags_Tooltip); // Mismatched BeginTooltip()/EndTooltip() calls + End(); +} + +// Mark popup as open (toggle toward open state). +// Popups are closed when user click outside, or activate a pressable item, or CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. +// Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). +// One open popup per level of the popup hierarchy (NB: when assigning we reset the Window member of ImGuiPopupRef to NULL) +void ImGui::OpenPopupEx(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* parent_window = g.CurrentWindow; + int current_stack_size = g.CurrentPopupStack.Size; + ImGuiPopupRef popup_ref; // Tagged as new ref as Window will be set back to NULL if we write this into OpenPopupStack. + popup_ref.PopupId = id; + popup_ref.Window = NULL; + popup_ref.ParentWindow = parent_window; + popup_ref.OpenFrameCount = g.FrameCount; + popup_ref.OpenParentId = parent_window->IDStack.back(); + popup_ref.OpenMousePos = g.IO.MousePos; + popup_ref.OpenPopupPos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : g.IO.MousePos; + + if (g.OpenPopupStack.Size < current_stack_size + 1) + { + g.OpenPopupStack.push_back(popup_ref); + } + else + { + // Close child popups if any + g.OpenPopupStack.resize(current_stack_size + 1); + + // Gently handle the user mistakenly calling OpenPopup() every frame. It is a programming mistake! However, if we were to run the regular code path, the ui + // would become completely unusable because the popup will always be in hidden-while-calculating-size state _while_ claiming focus. Which would be a very confusing + // situation for the programmer. Instead, we silently allow the popup to proceed, it will keep reappearing and the programming error will be more obvious to understand. + if (g.OpenPopupStack[current_stack_size].PopupId == id && g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) + g.OpenPopupStack[current_stack_size].OpenFrameCount = popup_ref.OpenFrameCount; + else + g.OpenPopupStack[current_stack_size] = popup_ref; + + // When reopening a popup we first refocus its parent, otherwise if its parent is itself a popup it would get closed by ClosePopupsOverWindow(). + // This is equivalent to what ClosePopupToLevel() does. + //if (g.OpenPopupStack[current_stack_size].PopupId == id) + // FocusWindow(parent_window); + } +} + +void ImGui::OpenPopup(const char* str_id) +{ + ImGuiContext& g = *GImGui; + OpenPopupEx(g.CurrentWindow->GetID(str_id)); +} + +void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.empty()) + return; + + // When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it. + // Don't close our own child popup windows. + int n = 0; + if (ref_window) + { + for (n = 0; n < g.OpenPopupStack.Size; n++) + { + ImGuiPopupRef& popup = g.OpenPopupStack[n]; + if (!popup.Window) + continue; + IM_ASSERT((popup.Window->Flags & ImGuiWindowFlags_Popup) != 0); + if (popup.Window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + + // Trim the stack if popups are not direct descendant of the reference window (which is often the NavWindow) + bool has_focus = false; + for (int m = n; m < g.OpenPopupStack.Size && !has_focus; m++) + has_focus = (g.OpenPopupStack[m].Window && g.OpenPopupStack[m].Window->RootWindow == ref_window->RootWindow); + if (!has_focus) + break; + } + } + if (n < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the block below + ClosePopupToLevel(n); +} + +static ImGuiWindow* GetFrontMostModalRootWindow() +{ + ImGuiContext& g = *GImGui; + for (int n = g.OpenPopupStack.Size-1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if (popup->Flags & ImGuiWindowFlags_Modal) + return popup; + return NULL; +} + +static void ClosePopupToLevel(int remaining) +{ + IM_ASSERT(remaining >= 0); + ImGuiContext& g = *GImGui; + ImGuiWindow* focus_window = (remaining > 0) ? g.OpenPopupStack[remaining-1].Window : g.OpenPopupStack[0].ParentWindow; + if (g.NavLayer == 0) + focus_window = NavRestoreLastChildNavWindow(focus_window); + ImGui::FocusWindow(focus_window); + focus_window->DC.NavHideHighlightOneFrame = true; + g.OpenPopupStack.resize(remaining); +} + +void ImGui::ClosePopup(ImGuiID id) +{ + if (!IsPopupOpen(id)) + return; + ImGuiContext& g = *GImGui; + ClosePopupToLevel(g.OpenPopupStack.Size - 1); +} + +// 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); +} + +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; + } + + 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(); + + return is_open; +} + +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); +} + +bool ImGui::IsPopupOpen(ImGuiID 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) +{ + 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) +{ + 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)); + + 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; +} + +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; + } +} + +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); + + // Make all menus and popups wrap around for now, may need to expose that policy. + NavProcessMoveRequestWrapAround(g.CurrentWindow); + + End(); +} + +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; +} + +// 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) +{ + IM_ASSERT(id != 0); + return BeginChildEx(NULL, id, size_arg, border, extra_flags); +} + +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() +{ + EndChild(); + PopStyleVar(3); + PopStyleColor(); +} + +// Save and compare stack sizes on Begin()/End() to detect usage errors +static void CheckStacksSize(ImGuiWindow* window, bool write) +{ + // NOT checking: DC.ItemWidth, DC.AllowKeyboardFocus, DC.ButtonRepeat, DC.TextWrapPos (per window) to allow user to conveniently push once and not pop (they are cleared on Begin) + ImGuiContext& g = *GImGui; + int* p_backup = &window->DC.StackSizesBackup[0]; + { int current = window->IDStack.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "PushID/PopID or TreeNode/TreePop Mismatch!"); p_backup++; } // Too few or too many PopID()/TreePop() + { int current = window->DC.GroupStack.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "BeginGroup/EndGroup Mismatch!"); p_backup++; } // Too few or too many EndGroup() + { int current = g.CurrentPopupStack.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "BeginMenu/EndMenu or BeginPopup/EndPopup Mismatch"); p_backup++;}// Too few or too many EndMenu()/EndPopup() + { int current = g.ColorModifiers.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "PushStyleColor/PopStyleColor Mismatch!"); p_backup++; } // Too few or too many PopStyleColor() + { int current = g.StyleModifiers.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "PushStyleVar/PopStyleVar Mismatch!"); p_backup++; } // Too few or too many PopStyleVar() + { int current = g.FontStack.Size; if (write) *p_backup = current; else IM_ASSERT(*p_backup == current && "PushFont/PopFont Mismatch!"); p_backup++; } // Too few or too many PopFont() + IM_ASSERT(p_backup == window->DC.StackSizesBackup + IM_ARRAYSIZE(window->DC.StackSizesBackup)); +} + +enum ImGuiPopupPositionPolicy +{ + ImGuiPopupPositionPolicy_Default, + ImGuiPopupPositionPolicy_ComboBox +}; + +static ImVec2 FindBestWindowPosForPopup(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy = ImGuiPopupPositionPolicy_Default) +{ + const ImGuiStyle& style = GImGui->Style; + + // r_avoid = the rectangle to avoid (e.g. for tooltip it is a rectangle around the mouse cursor which we want to avoid. for popups it's a small point around the cursor.) + // r_outer = the visible area rectangle, minus safe area padding. If our popup size won't fit because of safe area padding we ignore it. + ImVec2 safe_padding = style.DisplaySafeAreaPadding; + ImRect r_outer(GetViewportRect()); + r_outer.Expand(ImVec2((size.x - r_outer.GetWidth() > safe_padding.x*2) ? -safe_padding.x : 0.0f, (size.y - r_outer.GetHeight() > safe_padding.y*2) ? -safe_padding.y : 0.0f)); + ImVec2 base_pos_clamped = ImClamp(ref_pos, r_outer.Min, r_outer.Max - size); + //GImGui->OverlayDrawList.AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255,0,0,255)); + //GImGui->OverlayDrawList.AddRect(r_outer.Min, r_outer.Max, IM_COL32(0,255,0,255)); + + // Combo Box policy (we want a connecting edge) + if (policy == ImGuiPopupPositionPolicy_ComboBox) + { + const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = { ImGuiDir_Down, ImGuiDir_Right, ImGuiDir_Left, ImGuiDir_Up }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + ImVec2 pos; + if (dir == ImGuiDir_Down) pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y); // Below, Toward Right (default) + if (dir == ImGuiDir_Right) pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y); // Above, Toward Right + if (dir == ImGuiDir_Left) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y); // Below, Toward Left + if (dir == ImGuiDir_Up) pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y); // Above, Toward Left + if (!r_outer.Contains(ImRect(pos, pos + size))) + continue; + *last_dir = dir; + return pos; + } + } + + // Default popup policy + const ImGuiDir dir_prefered_order[ImGuiDir_Count_] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_Count_; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); + float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); + if (avail_w < size.x || avail_h < size.y) + continue; + ImVec2 pos; + pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; + pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; + *last_dir = dir; + return pos; + } + + // Fallback, try to keep within display + *last_dir = ImGuiDir_None; + ImVec2 pos = ref_pos; + pos.x = ImMax(ImMin(pos.x + size.x, r_outer.Max.x) - size.x, r_outer.Min.x); + pos.y = ImMax(ImMin(pos.y + size.y, r_outer.Max.y) - size.y, r_outer.Min.y); + return pos; +} + +static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) +{ + window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); + window->SetWindowSizeAllowFlags = enabled ? (window->SetWindowSizeAllowFlags | flags) : (window->SetWindowSizeAllowFlags & ~flags); + window->SetWindowCollapsedAllowFlags = enabled ? (window->SetWindowCollapsedAllowFlags | flags) : (window->SetWindowCollapsedAllowFlags & ~flags); +} + +ImGuiWindow* ImGui::FindWindowByName(const char* name) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = ImHash(name, 0); + return (ImGuiWindow*)g.WindowsById.GetVoidPtr(id); +} + +static ImGuiWindow* CreateNewWindow(const char* name, ImVec2 size, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + + // Create window the first time + ImGuiWindow* window = IM_NEW(ImGuiWindow)(&g, name); + window->Flags = flags; + g.WindowsById.SetVoidPtr(window->ID, window); + + // User can disable loading and saving of settings. Tooltip and child windows also don't store settings. + if (!(flags & ImGuiWindowFlags_NoSavedSettings)) + { + // Retrieve settings from .ini file + // Use SetWindowPos() or SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. + window->Pos = window->PosFloat = ImVec2(60, 60); + + if (ImGuiWindowSettings* settings = ImGui::FindWindowSettings(window->ID)) + { + SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); + window->PosFloat = settings->Pos; + window->Pos = ImFloor(window->PosFloat); + window->Collapsed = settings->Collapsed; + if (ImLengthSqr(settings->Size) > 0.00001f) + size = settings->Size; + } + } + window->Size = window->SizeFull = window->SizeFullAtLastBegin = size; + + if ((flags & ImGuiWindowFlags_AlwaysAutoResize) != 0) + { + window->AutoFitFramesX = window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = false; + } + else + { + if (window->Size.x <= 0.0f) + window->AutoFitFramesX = 2; + if (window->Size.y <= 0.0f) + window->AutoFitFramesY = 2; + window->AutoFitOnlyGrows = (window->AutoFitFramesX > 0) || (window->AutoFitFramesY > 0); + } + + if (flags & ImGuiWindowFlags_NoBringToFrontOnFocus) + g.Windows.insert(g.Windows.begin(), window); // Quite slow but rare and only once + else + g.Windows.push_back(window); + return window; +} + +static ImVec2 CalcSizeAfterConstraint(ImGuiWindow* window, ImVec2 new_size) +{ + ImGuiContext& g = *GImGui; + if (g.NextWindowData.SizeConstraintCond != 0) + { + // Using -1,-1 on either X/Y axis to preserve the current size. + ImRect cr = g.NextWindowData.SizeConstraintRect; + new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; + new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; + if (g.NextWindowData.SizeCallback) + { + ImGuiSizeCallbackData data; + data.UserData = g.NextWindowData.SizeCallbackUserData; + data.Pos = window->Pos; + data.CurrentSize = window->SizeFull; + data.DesiredSize = new_size; + g.NextWindowData.SizeCallback(&data); + new_size = data.DesiredSize; + } + } + + // Minimum size + if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) + { + new_size = ImMax(new_size, g.Style.WindowMinSize); + new_size.y = ImMax(new_size.y, window->TitleBarHeight() + window->MenuBarHeight() + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); // Reduce artifacts with very small windows + } + return new_size; +} + +static ImVec2 CalcSizeContents(ImGuiWindow* window) +{ + ImVec2 sz; + sz.x = (float)(int)((window->SizeContentsExplicit.x != 0.0f) ? window->SizeContentsExplicit.x : (window->DC.CursorMaxPos.x - window->Pos.x + window->Scroll.x)); + sz.y = (float)(int)((window->SizeContentsExplicit.y != 0.0f) ? window->SizeContentsExplicit.y : (window->DC.CursorMaxPos.y - window->Pos.y + window->Scroll.y)); + return sz + window->WindowPadding; +} + +static ImVec2 CalcSizeAutoFit(ImGuiWindow* window, const ImVec2& size_contents) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + ImGuiWindowFlags flags = window->Flags; + ImVec2 size_auto_fit; + if ((flags & ImGuiWindowFlags_Tooltip) != 0) + { + // Tooltip always resize. We keep the spacing symmetric on both axises for aesthetic purpose. + size_auto_fit = size_contents; + } + else + { + // When the window cannot fit all contents (either because of constraints, either because screen is too small): we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than DisplaySize-WindowPadding. + size_auto_fit = ImClamp(size_contents, style.WindowMinSize, ImMax(style.WindowMinSize, g.IO.DisplaySize - g.Style.DisplaySafeAreaPadding)); + ImVec2 size_auto_fit_after_constraint = CalcSizeAfterConstraint(window, size_auto_fit); + if (size_auto_fit_after_constraint.x < size_contents.x && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)) + size_auto_fit.y += style.ScrollbarSize; + if (size_auto_fit_after_constraint.y < size_contents.y && !(flags & ImGuiWindowFlags_NoScrollbar)) + size_auto_fit.x += style.ScrollbarSize; + } + return size_auto_fit; +} + +static float GetScrollMaxX(ImGuiWindow* window) +{ + return ImMax(0.0f, window->SizeContents.x - (window->SizeFull.x - window->ScrollbarSizes.x)); +} + +static float GetScrollMaxY(ImGuiWindow* window) +{ + return ImMax(0.0f, window->SizeContents.y - (window->SizeFull.y - window->ScrollbarSizes.y)); +} + +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window) +{ + ImVec2 scroll = window->Scroll; + float cr_x = window->ScrollTargetCenterRatio.x; + float cr_y = window->ScrollTargetCenterRatio.y; + if (window->ScrollTarget.x < FLT_MAX) + scroll.x = window->ScrollTarget.x - cr_x * (window->SizeFull.x - window->ScrollbarSizes.x); + if (window->ScrollTarget.y < FLT_MAX) + scroll.y = window->ScrollTarget.y - (1.0f - cr_y) * (window->TitleBarHeight() + window->MenuBarHeight()) - cr_y * (window->SizeFull.y - window->ScrollbarSizes.y); + scroll = ImMax(scroll, ImVec2(0.0f, 0.0f)); + if (!window->Collapsed && !window->SkipItems) + { + scroll.x = ImMin(scroll.x, GetScrollMaxX(window)); + scroll.y = ImMin(scroll.y, GetScrollMaxY(window)); + } + return scroll; +} + +static ImGuiCol GetWindowBgColorIdxFromFlags(ImGuiWindowFlags flags) +{ + if (flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) + return ImGuiCol_PopupBg; + if (flags & ImGuiWindowFlags_ChildWindow) + return ImGuiCol_ChildBg; + return ImGuiCol_WindowBg; +} + +static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& corner_target, const ImVec2& corner_norm, ImVec2* out_pos, ImVec2* out_size) +{ + ImVec2 pos_min = ImLerp(corner_target, window->Pos, corner_norm); // Expected window upper-left + ImVec2 pos_max = ImLerp(window->Pos + window->Size, corner_target, corner_norm); // Expected window lower-right + ImVec2 size_expected = pos_max - pos_min; + ImVec2 size_constrained = CalcSizeAfterConstraint(window, size_expected); + *out_pos = pos_min; + if (corner_norm.x == 0.0f) + out_pos->x -= (size_constrained.x - size_expected.x); + if (corner_norm.y == 0.0f) + out_pos->y -= (size_constrained.y - size_expected.y); + *out_size = size_constrained; +} + +struct ImGuiResizeGripDef +{ + ImVec2 CornerPos; + ImVec2 InnerDir; + int AngleMin12, AngleMax12; +}; + +const ImGuiResizeGripDef resize_grip_def[4] = +{ + { ImVec2(1,1), ImVec2(-1,-1), 0, 3 }, // Lower right + { ImVec2(0,1), ImVec2(+1,-1), 3, 6 }, // Lower left + { ImVec2(0,0), ImVec2(+1,+1), 6, 9 }, // Upper left + { ImVec2(1,0), ImVec2(-1,+1), 9,12 }, // Upper right +}; + +static ImRect GetBorderRect(ImGuiWindow* window, int border_n, float perp_padding, float thickness) +{ + 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; +} + +// Push a new ImGui window to add widgets to. +// - A default window called "Debug" is automatically stacked at the beginning of every frame so you can use widgets without explicitly calling a Begin/End pair. +// - Begin/End can be called multiple times during the frame with the same window name to append content. +// - The window name is used as a unique identifier to preserve window information across frames (and save rudimentary information to the .ini file). +// You can use the "##" or "###" markers to use the same label with different id, or same id with different label. See documentation at the top of this file. +// - Return false when window is collapsed, so you can early out in your code. You always need to call ImGui::End() even if false is returned. +// - Passing 'bool* p_open' displays a Close button on the upper-right corner of the window, the pointed value will be set to false when the button is pressed. +bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + IM_ASSERT(name != NULL); // Window name required + IM_ASSERT(g.Initialized); // Forgot to call ImGui::NewFrame() + IM_ASSERT(g.FrameCountEnded != g.FrameCount); // Called ImGui::Render() or ImGui::EndFrame() and haven't called ImGui::NewFrame() again yet + + // Find or create + ImGuiWindow* window = FindWindowByName(name); + if (!window) + { + ImVec2 size_on_first_use = (g.NextWindowData.SizeCond != 0) ? g.NextWindowData.SizeVal : ImVec2(0.0f, 0.0f); // Any condition flag will do since we are creating a new window here. + window = CreateNewWindow(name, size_on_first_use, flags); + } + + // Automatically disable manual moving/resizing when NoInputs is set + if (flags & ImGuiWindowFlags_NoInputs) + flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; + + if (flags & ImGuiWindowFlags_NavFlattened) + IM_ASSERT(flags & ImGuiWindowFlags_ChildWindow); + + const int current_frame = g.FrameCount; + const bool first_begin_of_the_frame = (window->LastFrameActive != current_frame); + if (first_begin_of_the_frame) + window->Flags = (ImGuiWindowFlags)flags; + else + flags = window->Flags; + + // Update the Appearing flag + bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on + const bool window_just_appearing_after_hidden_for_resize = (window->HiddenFrames == 1); + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupRef& popup_ref = g.OpenPopupStack[g.CurrentPopupStack.Size]; + window_just_activated_by_user |= (window->PopupId != popup_ref.PopupId); // We recycle popups so treat window as activated if popup id changed + window_just_activated_by_user |= (window != popup_ref.Window); + } + window->Appearing = (window_just_activated_by_user || window_just_appearing_after_hidden_for_resize); + window->CloseButton = (p_open != NULL); + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); + + // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack + ImGuiWindow* parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back(); + ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow; + IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow)); + + // Add to stack + g.CurrentWindowStack.push_back(window); + SetCurrentWindow(window); + CheckStacksSize(window, true); + if (flags & ImGuiWindowFlags_Popup) + { + ImGuiPopupRef& popup_ref = g.OpenPopupStack[g.CurrentPopupStack.Size]; + popup_ref.Window = window; + g.CurrentPopupStack.push_back(popup_ref); + window->PopupId = popup_ref.PopupId; + } + + if (window_just_appearing_after_hidden_for_resize && !(flags & ImGuiWindowFlags_ChildWindow)) + window->NavLastIds[0] = 0; + + // Process SetNextWindow***() calls + bool window_pos_set_by_api = false; + bool window_size_x_set_by_api = false, window_size_y_set_by_api = false; + if (g.NextWindowData.PosCond) + { + window_pos_set_by_api = (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) != 0; + if (window_pos_set_by_api && ImLengthSqr(g.NextWindowData.PosPivotVal) > 0.00001f) + { + // May be processed on the next frame if this is our first frame and we are measuring size + // FIXME: Look into removing the branch so everything can go through this same code path for consistency. + window->SetWindowPosVal = g.NextWindowData.PosVal; + window->SetWindowPosPivot = g.NextWindowData.PosPivotVal; + window->SetWindowPosAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + } + else + { + SetWindowPos(window, g.NextWindowData.PosVal, g.NextWindowData.PosCond); + } + g.NextWindowData.PosCond = 0; + } + if (g.NextWindowData.SizeCond) + { + window_size_x_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.x > 0.0f); + window_size_y_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.y > 0.0f); + SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond); + g.NextWindowData.SizeCond = 0; + } + if (g.NextWindowData.ContentSizeCond) + { + // Adjust passed "client size" to become a "window size" + window->SizeContentsExplicit = g.NextWindowData.ContentSizeVal; + if (window->SizeContentsExplicit.y != 0.0f) + window->SizeContentsExplicit.y += window->TitleBarHeight() + window->MenuBarHeight(); + g.NextWindowData.ContentSizeCond = 0; + } + else if (first_begin_of_the_frame) + { + window->SizeContentsExplicit = ImVec2(0.0f, 0.0f); + } + if (g.NextWindowData.CollapsedCond) + { + SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond); + g.NextWindowData.CollapsedCond = 0; + } + if (g.NextWindowData.FocusCond) + { + SetWindowFocus(); + g.NextWindowData.FocusCond = 0; + } + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); + + // When reusing window again multiple times a frame, just append content (don't need to setup again) + if (first_begin_of_the_frame) + { + const bool window_is_child_tooltip = (flags & ImGuiWindowFlags_ChildWindow) && (flags & ImGuiWindowFlags_Tooltip); // FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345) + + // Initialize + window->ParentWindow = parent_window; + window->RootWindow = window->RootWindowForTitleBarHighlight = window->RootWindowForTabbing = window->RootWindowForNav = window; + if (parent_window && (flags & ImGuiWindowFlags_ChildWindow) && !window_is_child_tooltip) + window->RootWindow = parent_window->RootWindow; + if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) + window->RootWindowForTitleBarHighlight = window->RootWindowForTabbing = parent_window->RootWindowForTitleBarHighlight; // Same value in master branch, will differ for docking + while (window->RootWindowForNav->Flags & ImGuiWindowFlags_NavFlattened) + window->RootWindowForNav = window->RootWindowForNav->ParentWindow; + + window->Active = true; + window->BeginOrderWithinParent = 0; + window->BeginOrderWithinContext = g.WindowsActiveCount++; + window->BeginCount = 0; + window->ClipRect = ImVec4(-FLT_MAX,-FLT_MAX,+FLT_MAX,+FLT_MAX); + window->LastFrameActive = current_frame; + window->IDStack.resize(1); + + // Lock window rounding, border size and rounding so that altering the border sizes for children doesn't have side-effects. + window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; + window->WindowBorderSize = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildBorderSize : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupBorderSize : style.WindowBorderSize; + window->WindowPadding = style.WindowPadding; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & (ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_Popup)) && window->WindowBorderSize == 0.0f) + window->WindowPadding = ImVec2(0.0f, (flags & ImGuiWindowFlags_MenuBar) ? style.WindowPadding.y : 0.0f); + + // Collapse window by double-clicking on title bar + // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse)) + { + ImRect title_bar_rect = window->TitleBarRect(); + if (window->CollapseToggleWanted || (g.HoveredWindow == window && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max) && g.IO.MouseDoubleClicked[0])) + { + window->Collapsed = !window->Collapsed; + MarkIniSettingsDirty(window); + FocusWindow(window); + } + } + else + { + window->Collapsed = false; + } + window->CollapseToggleWanted = false; + + // SIZE + + // Update contents size from last frame for auto-fitting (unless explicitly specified) + window->SizeContents = CalcSizeContents(window); + + // Hide popup/tooltip window when re-opening while we measure size (because we recycle the windows) + if (window->HiddenFrames > 0) + window->HiddenFrames--; + if ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) != 0 && window_just_activated_by_user) + { + window->HiddenFrames = 1; + if (flags & ImGuiWindowFlags_AlwaysAutoResize) + { + if (!window_size_x_set_by_api) + window->Size.x = window->SizeFull.x = 0.f; + if (!window_size_y_set_by_api) + window->Size.y = window->SizeFull.y = 0.f; + window->SizeContents = ImVec2(0.f, 0.f); + } + } + + // Calculate auto-fit size, handle automatic resize + const ImVec2 size_auto_fit = CalcSizeAutoFit(window, window->SizeContents); + ImVec2 size_full_modified(FLT_MAX, FLT_MAX); + if (flags & ImGuiWindowFlags_AlwaysAutoResize && !window->Collapsed) + { + // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. + if (!window_size_x_set_by_api) + window->SizeFull.x = size_full_modified.x = size_auto_fit.x; + if (!window_size_y_set_by_api) + window->SizeFull.y = size_full_modified.y = size_auto_fit.y; + } + else if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + { + // Auto-fit only grows during the first few frames + // We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags_AlwaysAutoResize when collapsed. + if (!window_size_x_set_by_api && window->AutoFitFramesX > 0) + window->SizeFull.x = size_full_modified.x = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.x, size_auto_fit.x) : size_auto_fit.x; + if (!window_size_y_set_by_api && window->AutoFitFramesY > 0) + window->SizeFull.y = size_full_modified.y = window->AutoFitOnlyGrows ? ImMax(window->SizeFull.y, size_auto_fit.y) : size_auto_fit.y; + if (!window->Collapsed) + MarkIniSettingsDirty(window); + } + + // Apply minimum/maximum window size constraints and final size + window->SizeFull = CalcSizeAfterConstraint(window, window->SizeFull); + window->Size = window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow) ? window->TitleBarRect().GetSize() : window->SizeFull; + + // SCROLLBAR STATUS + + // Update scrollbar status (based on the Size that was effective during last frame or the auto-resized Size). + if (!window->Collapsed) + { + // When reading the current size we need to read it after size constraints have been applied + float size_x_for_scrollbars = size_full_modified.x != FLT_MAX ? window->SizeFull.x : window->SizeFullAtLastBegin.x; + float size_y_for_scrollbars = size_full_modified.y != FLT_MAX ? window->SizeFull.y : window->SizeFullAtLastBegin.y; + window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((window->SizeContents.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); + window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((window->SizeContents.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); + if (window->ScrollbarX && !window->ScrollbarY) + window->ScrollbarY = (window->SizeContents.y > size_y_for_scrollbars - style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); + window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); + } + + // POSITION + + // Popup latch its initial position, will position itself when it appears next frame + if (window_just_activated_by_user) + { + window->AutoPosLastDirection = ImGuiDir_None; + if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api) + window->Pos = window->PosFloat = g.CurrentPopupStack.back().OpenPopupPos; + } + + // Position child window + if (flags & ImGuiWindowFlags_ChildWindow) + { + window->BeginOrderWithinParent = parent_window->DC.ChildWindows.Size; + parent_window->DC.ChildWindows.push_back(window); + if (!(flags & ImGuiWindowFlags_Popup) && !window_pos_set_by_api && !window_is_child_tooltip) + window->Pos = window->PosFloat = parent_window->DC.CursorPos; + } + + const bool window_pos_with_pivot = (window->SetWindowPosVal.x != FLT_MAX && window->HiddenFrames == 0); + if (window_pos_with_pivot) + { + // Position given a pivot (e.g. for centering) + SetWindowPos(window, ImMax(style.DisplaySafeAreaPadding, window->SetWindowPosVal - window->SizeFull * window->SetWindowPosPivot), 0); + } + else if (flags & ImGuiWindowFlags_ChildMenu) + { + // Child menus typically request _any_ position within the parent menu item, and then our FindBestPopupWindowPos() function will move the new menu outside the parent bounds. + // This is how we end up with child menus appearing (most-commonly) on the right of the parent menu. + IM_ASSERT(window_pos_set_by_api); + float horizontal_overlap = style.ItemSpacing.x; // We want some overlap to convey the relative depth of each popup (currently the amount of overlap it is hard-coded to style.ItemSpacing.x, may need to introduce another style value). + ImGuiWindow* parent_menu = parent_window_in_stack; + ImRect rect_to_avoid; + if (parent_menu->DC.MenuBarAppending) + rect_to_avoid = ImRect(-FLT_MAX, parent_menu->Pos.y + parent_menu->TitleBarHeight(), FLT_MAX, parent_menu->Pos.y + parent_menu->TitleBarHeight() + parent_menu->MenuBarHeight()); + else + rect_to_avoid = ImRect(parent_menu->Pos.x + horizontal_overlap, -FLT_MAX, parent_menu->Pos.x + parent_menu->Size.x - horizontal_overlap - parent_menu->ScrollbarSizes.x, FLT_MAX); + window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + } + else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) + { + ImRect rect_to_avoid(window->PosFloat.x - 1, window->PosFloat.y - 1, window->PosFloat.x + 1, window->PosFloat.y + 1); + window->PosFloat = FindBestWindowPosForPopup(window->PosFloat, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + } + + // Position tooltip (always follows mouse) + if ((flags & ImGuiWindowFlags_Tooltip) != 0 && !window_pos_set_by_api && !window_is_child_tooltip) + { + float sc = g.Style.MouseCursorScale; + ImVec2 ref_pos = (!g.NavDisableHighlight && g.NavDisableMouseHover) ? NavCalcPreferredMousePos() : g.IO.MousePos; + ImRect rect_to_avoid; + if (!g.NavDisableHighlight && g.NavDisableMouseHover && !(g.IO.NavFlags & ImGuiNavFlags_MoveMouse)) + rect_to_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8); + else + rect_to_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * sc, ref_pos.y + 24 * sc); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. + window->PosFloat = FindBestWindowPosForPopup(ref_pos, window->Size, &window->AutoPosLastDirection, rect_to_avoid); + if (window->AutoPosLastDirection == ImGuiDir_None) + window->PosFloat = ref_pos + ImVec2(2,2); // If there's not enough room, for tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. + } + + // Clamp position so it stays visible + if (!(flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Tooltip)) + { + if (!window_pos_set_by_api && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && g.IO.DisplaySize.x > 0.0f && g.IO.DisplaySize.y > 0.0f) // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. + { + ImVec2 padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); + window->PosFloat = ImMax(window->PosFloat + window->Size, padding) - window->Size; + window->PosFloat = ImMin(window->PosFloat, g.IO.DisplaySize - padding); + } + } + window->Pos = ImFloor(window->PosFloat); + + // Default item width. Make it proportional to window size if window manually resizes + if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)) + window->ItemWidthDefault = (float)(int)(window->Size.x * 0.65f); + else + window->ItemWidthDefault = (float)(int)(g.FontSize * 16.0f); + + // Prepare for focus requests + window->FocusIdxAllRequestCurrent = (window->FocusIdxAllRequestNext == INT_MAX || window->FocusIdxAllCounter == -1) ? INT_MAX : (window->FocusIdxAllRequestNext + (window->FocusIdxAllCounter+1)) % (window->FocusIdxAllCounter+1); + window->FocusIdxTabRequestCurrent = (window->FocusIdxTabRequestNext == INT_MAX || window->FocusIdxTabCounter == -1) ? INT_MAX : (window->FocusIdxTabRequestNext + (window->FocusIdxTabCounter+1)) % (window->FocusIdxTabCounter+1); + window->FocusIdxAllCounter = window->FocusIdxTabCounter = -1; + window->FocusIdxAllRequestNext = window->FocusIdxTabRequestNext = INT_MAX; + + // Apply scrolling + window->Scroll = CalcNextScrollFromScrollTargetAndClamp(window); + window->ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + + // Apply focus, new windows appears in front + bool want_focus = false; + if (window_just_activated_by_user && !(flags & ImGuiWindowFlags_NoFocusOnAppearing)) + if (!(flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Tooltip)) || (flags & ImGuiWindowFlags_Popup)) + want_focus = true; + + // Handle manual resize: Resize Grips, Borders, Gamepad + int border_held = -1; + ImU32 resize_grip_col[4] = { 0 }; + const int resize_grip_count = (flags & ImGuiWindowFlags_ResizeFromAnySide) ? 2 : 1; // 4 + const float grip_draw_size = (float)(int)ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f); + if (!window->Collapsed) + UpdateManualResize(window, size_auto_fit, &border_held, resize_grip_count, &resize_grip_col[0]); + + // DRAWING + + // Setup draw list and outer clipping rectangle + window->DrawList->Clear(); + window->DrawList->Flags = (g.Style.AntiAliasedLines ? ImDrawListFlags_AntiAliasedLines : 0) | (g.Style.AntiAliasedFill ? ImDrawListFlags_AntiAliasedFill : 0); + window->DrawList->PushTextureID(g.Font->ContainerAtlas->TexID); + ImRect viewport_rect(GetViewportRect()); + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) + PushClipRect(parent_window->ClipRect.Min, parent_window->ClipRect.Max, true); + else + PushClipRect(viewport_rect.Min, viewport_rect.Max, true); + + // Draw modal window background (darkens what is behind them) + if ((flags & ImGuiWindowFlags_Modal) != 0 && window == GetFrontMostModalRootWindow()) + window->DrawList->AddRectFilled(viewport_rect.Min, viewport_rect.Max, GetColorU32(ImGuiCol_ModalWindowDarkening, g.ModalWindowDarkeningRatio)); + + // Draw navigation selection/windowing rectangle background + if (g.NavWindowingTarget == window) + { + ImRect bb = window->Rect(); + bb.Expand(g.FontSize); + if (!bb.Contains(viewport_rect)) // Avoid drawing if the window covers all the viewport anyway + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha * 0.25f), g.Style.WindowRounding); + } + + // Draw window + handle manual resize + const float window_rounding = window->WindowRounding; + const float window_border_size = window->WindowBorderSize; + const bool title_bar_is_highlight = want_focus || (g.NavWindow && window->RootWindowForTitleBarHighlight == g.NavWindow->RootWindowForTitleBarHighlight); + const ImRect title_bar_rect = window->TitleBarRect(); + if (window->Collapsed) + { + // Title bar only + float backup_border_size = style.FrameBorderSize; + g.Style.FrameBorderSize = window->WindowBorderSize; + ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && !g.NavDisableHighlight) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); + RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding); + g.Style.FrameBorderSize = backup_border_size; + } + else + { + // Window background + ImU32 bg_col = GetColorU32(GetWindowBgColorIdxFromFlags(flags)); + if (g.NextWindowData.BgAlphaCond != 0) + { + bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(g.NextWindowData.BgAlphaVal) << IM_COL32_A_SHIFT); + g.NextWindowData.BgAlphaCond = 0; + } + window->DrawList->AddRectFilled(window->Pos+ImVec2(0,window->TitleBarHeight()), window->Pos+window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Bot); + + // Title bar + ImU32 title_bar_col = GetColorU32(window->Collapsed ? ImGuiCol_TitleBgCollapsed : title_bar_is_highlight ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawCornerFlags_Top); + + // Menu bar + if (flags & ImGuiWindowFlags_MenuBar) + { + ImRect menu_bar_rect = window->MenuBarRect(); + menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them. + window->DrawList->AddRectFilled(menu_bar_rect.Min, menu_bar_rect.Max, GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawCornerFlags_Top); + if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y) + window->DrawList->AddLine(menu_bar_rect.GetBL(), menu_bar_rect.GetBR(), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + } + + // Scrollbars + if (window->ScrollbarX) + Scrollbar(ImGuiLayoutType_Horizontal); + if (window->ScrollbarY) + Scrollbar(ImGuiLayoutType_Vertical); + + // Render resize grips (after their input handling so we don't have a frame of latency) + if (!(flags & ImGuiWindowFlags_NoResize)) + { + for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) + { + const ImGuiResizeGripDef& grip = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPos); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(window_border_size, grip_draw_size) : ImVec2(grip_draw_size, window_border_size))); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(grip_draw_size, window_border_size) : ImVec2(window_border_size, grip_draw_size))); + window->DrawList->PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + window_border_size), corner.y + grip.InnerDir.y * (window_rounding + window_border_size)), window_rounding, grip.AngleMin12, grip.AngleMax12); + window->DrawList->PathFillConvex(resize_grip_col[resize_grip_n]); + } + } + + // Borders + if (window_border_size > 0.0f) + window->DrawList->AddRect(window->Pos, window->Pos+window->Size, GetColorU32(ImGuiCol_Border), window_rounding, ImDrawCornerFlags_All, window_border_size); + if (border_held != -1) + { + ImRect border = GetBorderRect(window, border_held, grip_draw_size, 0.0f); + window->DrawList->AddLine(border.Min, border.Max, GetColorU32(ImGuiCol_SeparatorActive), ImMax(1.0f, window_border_size)); + } + if (style.FrameBorderSize > 0 && !(flags & ImGuiWindowFlags_NoTitleBar)) + window->DrawList->AddLine(title_bar_rect.GetBL() + ImVec2(style.WindowBorderSize, -1), title_bar_rect.GetBR() + ImVec2(-style.WindowBorderSize,-1), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + } + + // Draw navigation selection/windowing rectangle border + if (g.NavWindowingTarget == window) + { + float rounding = ImMax(window->WindowRounding, g.Style.WindowRounding); + ImRect bb = window->Rect(); + bb.Expand(g.FontSize); + if (bb.Contains(viewport_rect)) // If a window fits the entire viewport, adjust its highlight inward + { + bb.Expand(-g.FontSize - 1.0f); + rounding = window->WindowRounding; + } + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), rounding, ~0, 3.0f); + } + + // Store a backup of SizeFull which we will use next frame to decide if we need scrollbars. + window->SizeFullAtLastBegin = window->SizeFull; + + // Update ContentsRegionMax. All the variable it depends on are set above in this function. + window->ContentsRegionRect.Min.x = -window->Scroll.x + window->WindowPadding.x; + window->ContentsRegionRect.Min.y = -window->Scroll.y + window->WindowPadding.y + window->TitleBarHeight() + window->MenuBarHeight(); + window->ContentsRegionRect.Max.x = -window->Scroll.x - window->WindowPadding.x + (window->SizeContentsExplicit.x != 0.0f ? window->SizeContentsExplicit.x : (window->Size.x - window->ScrollbarSizes.x)); + window->ContentsRegionRect.Max.y = -window->Scroll.y - window->WindowPadding.y + (window->SizeContentsExplicit.y != 0.0f ? window->SizeContentsExplicit.y : (window->Size.y - window->ScrollbarSizes.y)); + + // Setup drawing context + // (NB: That term "drawing context / DC" lost its meaning a long time ago. Initially was meant to hold transient data only. Nowadays difference between window-> and window->DC-> is dubious.) + window->DC.IndentX = 0.0f + window->WindowPadding.x - window->Scroll.x; + window->DC.GroupOffsetX = 0.0f; + window->DC.ColumnsOffsetX = 0.0f; + window->DC.CursorStartPos = window->Pos + ImVec2(window->DC.IndentX + window->DC.ColumnsOffsetX, window->TitleBarHeight() + window->MenuBarHeight() + window->WindowPadding.y - window->Scroll.y); + window->DC.CursorPos = window->DC.CursorStartPos; + window->DC.CursorPosPrevLine = window->DC.CursorPos; + window->DC.CursorMaxPos = window->DC.CursorStartPos; + window->DC.CurrentLineHeight = window->DC.PrevLineHeight = 0.0f; + window->DC.CurrentLineTextBaseOffset = window->DC.PrevLineTextBaseOffset = 0.0f; + window->DC.NavHideHighlightOneFrame = false; + window->DC.NavHasScroll = (GetScrollMaxY() > 0.0f); + window->DC.NavLayerActiveMask = window->DC.NavLayerActiveMaskNext; + window->DC.NavLayerActiveMaskNext = 0x00; + window->DC.MenuBarAppending = false; + window->DC.MenuBarOffsetX = ImMax(window->WindowPadding.x, style.ItemSpacing.x); + window->DC.LogLinePosY = window->DC.CursorPos.y - 9999.0f; + window->DC.ChildWindows.resize(0); + window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical; + window->DC.ItemFlags = ImGuiItemFlags_Default_; + window->DC.ItemWidth = window->ItemWidthDefault; + window->DC.TextWrapPos = -1.0f; // disabled + window->DC.ItemFlagsStack.resize(0); + window->DC.ItemWidthStack.resize(0); + window->DC.TextWrapPosStack.resize(0); + window->DC.ColumnsSet = NULL; + window->DC.TreeDepth = 0; + window->DC.TreeDepthMayJumpToParentOnPop = 0x00; + window->DC.StateStorage = &window->StateStorage; + window->DC.GroupStack.resize(0); + window->MenuColumns.Update(3, style.ItemSpacing.x, window_just_activated_by_user); + + if ((flags & ImGuiWindowFlags_ChildWindow) && (window->DC.ItemFlags != parent_window->DC.ItemFlags)) + { + window->DC.ItemFlags = parent_window->DC.ItemFlags; + window->DC.ItemFlagsStack.push_back(window->DC.ItemFlags); + } + + if (window->AutoFitFramesX > 0) + window->AutoFitFramesX--; + if (window->AutoFitFramesY > 0) + window->AutoFitFramesY--; + + // Apply focus (we need to call FocusWindow() AFTER setting DC.CursorStartPos so our initial navigation reference rectangle can start around there) + if (want_focus) + { + FocusWindow(window); + NavInitWindow(window, false); + } + + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar)) + { + // Close & collapse button are on layer 1 (same as menus) and don't default focus + const ImGuiItemFlags item_flags_backup = window->DC.ItemFlags; + window->DC.ItemFlags |= ImGuiItemFlags_NoNavDefaultFocus; + window->DC.NavLayerCurrent++; + window->DC.NavLayerCurrentMask <<= 1; + + // Collapse button + if (!(flags & ImGuiWindowFlags_NoCollapse)) + { + ImGuiID id = window->GetID("#COLLAPSE"); + ImRect bb(window->Pos + style.FramePadding + ImVec2(1,1), window->Pos + style.FramePadding + ImVec2(g.FontSize,g.FontSize) - ImVec2(1,1)); + ItemAdd(bb, id); // To allow navigation + if (ButtonBehavior(bb, id, NULL, NULL)) + window->CollapseToggleWanted = true; // Defer collapsing to next frame as we are too far in the Begin() function + RenderNavHighlight(bb, id); + RenderTriangle(window->Pos + style.FramePadding, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); + } + + // Close button + if (p_open != NULL) + { + const float PAD = 2.0f; + const float rad = (window->TitleBarHeight() - PAD*2.0f) * 0.5f; + if (CloseButton(window->GetID("#CLOSE"), window->Rect().GetTR() + ImVec2(-PAD - rad, PAD + rad), rad)) + *p_open = false; + } + + window->DC.NavLayerCurrent--; + window->DC.NavLayerCurrentMask >>= 1; + window->DC.ItemFlags = item_flags_backup; + + // Title text (FIXME: refactor text alignment facilities along with RenderText helpers) + ImVec2 text_size = CalcTextSize(name, NULL, true); + ImRect text_r = title_bar_rect; + float pad_left = (flags & ImGuiWindowFlags_NoCollapse) == 0 ? (style.FramePadding.x + g.FontSize + style.ItemInnerSpacing.x) : style.FramePadding.x; + float pad_right = (p_open != NULL) ? (style.FramePadding.x + g.FontSize + style.ItemInnerSpacing.x) : style.FramePadding.x; + if (style.WindowTitleAlign.x > 0.0f) pad_right = ImLerp(pad_right, pad_left, style.WindowTitleAlign.x); + text_r.Min.x += pad_left; + text_r.Max.x -= pad_right; + ImRect clip_rect = text_r; + clip_rect.Max.x = window->Pos.x + window->Size.x - (p_open ? title_bar_rect.GetHeight() - 3 : style.FramePadding.x); // Match the size of CloseButton() + RenderTextClipped(text_r.Min, text_r.Max, name, NULL, &text_size, style.WindowTitleAlign, &clip_rect); + } + + // Save clipped aabb so we can access it in constant-time in FindHoveredWindow() + window->WindowRectClipped = window->Rect(); + window->WindowRectClipped.ClipWith(window->ClipRect); + + // Pressing CTRL+C while holding on a window copy its content to the clipboard + // This works but 1. doesn't handle multiple Begin/End pairs, 2. recursing into another Begin/End pair - so we need to work that out and add better logging scope. + // Maybe we can support CTRL+C on every element? + /* + if (g.ActiveId == move_id) + if (g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_C)) + ImGui::LogToClipboard(); + */ + + // Inner rectangle + // We set this up after processing the resize grip so that our clip rectangle doesn't lag by a frame + // Note that if our window is collapsed we will end up with a null clipping rectangle which is the correct behavior. + window->InnerRect.Min.x = title_bar_rect.Min.x + window->WindowBorderSize; + window->InnerRect.Min.y = title_bar_rect.Max.y + window->MenuBarHeight() + (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); + window->InnerRect.Max.x = window->Pos.x + window->Size.x - window->ScrollbarSizes.x - window->WindowBorderSize; + window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->ScrollbarSizes.y - window->WindowBorderSize; + //window->DrawList->AddRect(window->InnerRect.Min, window->InnerRect.Max, IM_COL32_WHITE); + + // After Begin() we fill the last item / hovered data using the title bar data. Make that a standard behavior (to allow usage of context menus on title bar only, etc.). + window->DC.LastItemId = window->MoveId; + window->DC.LastItemStatusFlags = IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0; + window->DC.LastItemRect = title_bar_rect; + } + + // Inner clipping rectangle + // Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. + const float border_size = window->WindowBorderSize; + ImRect clip_rect; + clip_rect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + ImMax(0.0f, ImFloor(window->WindowPadding.x*0.5f - border_size))); + clip_rect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y); + clip_rect.Max.x = ImFloor(0.5f + window->InnerRect.Max.x - ImMax(0.0f, ImFloor(window->WindowPadding.x*0.5f - border_size))); + clip_rect.Max.y = ImFloor(0.5f + window->InnerRect.Max.y); + PushClipRect(clip_rect.Min, clip_rect.Max, true); + + // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) + if (first_begin_of_the_frame) + window->WriteAccessed = false; + + window->BeginCount++; + g.NextWindowData.SizeConstraintCond = 0; + + // Child window can be out of sight and have "negative" clip windows. + // Mark them as collapsed so commands are skipped earlier (we can't manually collapse because they have no title bar). + if (flags & ImGuiWindowFlags_ChildWindow) + { + IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0); + window->Collapsed = parent_window && parent_window->Collapsed; + + if (!(flags & ImGuiWindowFlags_AlwaysAutoResize) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) + window->Collapsed |= (window->WindowRectClipped.Min.x >= window->WindowRectClipped.Max.x || window->WindowRectClipped.Min.y >= window->WindowRectClipped.Max.y); + + // We also hide the window from rendering because we've already added its border to the command list. + // (we could perform the check earlier in the function but it is simpler at this point) + if (window->Collapsed) + window->Active = false; + } + if (style.Alpha <= 0.0f) + window->Active = false; + + // Return false if we don't intend to display anything to allow user to perform an early out optimization + window->SkipItems = (window->Collapsed || !window->Active) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0; + return !window->SkipItems; +} + +// Old Begin() API with 5 parameters, avoid calling this version directly! Use SetNextWindowSize()/SetNextWindowBgAlpha() + Begin() instead. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +bool ImGui::Begin(const char* name, bool* p_open, const ImVec2& size_first_use, float bg_alpha_override, ImGuiWindowFlags flags) +{ + // Old API feature: we could pass the initial window size as a parameter. This was misleading because it only had an effect if the window didn't have data in the .ini file. + if (size_first_use.x != 0.0f || size_first_use.y != 0.0f) + ImGui::SetNextWindowSize(size_first_use, ImGuiCond_FirstUseEver); + + // Old API feature: override the window background alpha with a parameter. + if (bg_alpha_override >= 0.0f) + ImGui::SetNextWindowBgAlpha(bg_alpha_override); + + return ImGui::Begin(name, p_open, flags); +} +#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +void ImGui::End() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (window->DC.ColumnsSet != NULL) + EndColumns(); + PopClipRect(); // Inner window clip rectangle + + // Stop logging + if (!(window->Flags & ImGuiWindowFlags_ChildWindow)) // FIXME: add more options for scope of logging + LogFinish(); + + // Pop from window stack + g.CurrentWindowStack.pop_back(); + if (window->Flags & ImGuiWindowFlags_Popup) + g.CurrentPopupStack.pop_back(); + CheckStacksSize(window, false); + SetCurrentWindow(g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back()); +} + +// Vertical scrollbar +// The entire piece of code below is rather confusing because: +// - We handle absolute seeking (when first clicking outside the grab) and relative manipulation (afterward or when clicking inside the grab) +// - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar +// - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal. +void ImGui::Scrollbar(ImGuiLayoutType direction) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + const bool horizontal = (direction == ImGuiLayoutType_Horizontal); + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(horizontal ? "#SCROLLX" : "#SCROLLY"); + + // Render background + bool other_scrollbar = (horizontal ? window->ScrollbarY : window->ScrollbarX); + float other_scrollbar_size_w = other_scrollbar ? style.ScrollbarSize : 0.0f; + const ImRect window_rect = window->Rect(); + const float border_size = window->WindowBorderSize; + ImRect bb = horizontal + ? ImRect(window->Pos.x + border_size, window_rect.Max.y - style.ScrollbarSize, window_rect.Max.x - other_scrollbar_size_w - border_size, window_rect.Max.y - border_size) + : ImRect(window_rect.Max.x - style.ScrollbarSize, window->Pos.y + border_size, window_rect.Max.x - border_size, window_rect.Max.y - other_scrollbar_size_w - border_size); + if (!horizontal) + bb.Min.y += window->TitleBarHeight() + ((window->Flags & ImGuiWindowFlags_MenuBar) ? window->MenuBarHeight() : 0.0f); + if (bb.GetWidth() <= 0.0f || bb.GetHeight() <= 0.0f) + return; + + int window_rounding_corners; + if (horizontal) + window_rounding_corners = ImDrawCornerFlags_BotLeft | (other_scrollbar ? 0 : ImDrawCornerFlags_BotRight); + else + window_rounding_corners = (((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) ? ImDrawCornerFlags_TopRight : 0) | (other_scrollbar ? 0 : ImDrawCornerFlags_BotRight); + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_ScrollbarBg), window->WindowRounding, window_rounding_corners); + bb.Expand(ImVec2(-ImClamp((float)(int)((bb.Max.x - bb.Min.x - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp((float)(int)((bb.Max.y - bb.Min.y - 2.0f) * 0.5f), 0.0f, 3.0f))); + + // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) + float scrollbar_size_v = horizontal ? bb.GetWidth() : bb.GetHeight(); + float scroll_v = horizontal ? window->Scroll.x : window->Scroll.y; + float win_size_avail_v = (horizontal ? window->SizeFull.x : window->SizeFull.y) - other_scrollbar_size_w; + float win_size_contents_v = horizontal ? window->SizeContents.x : window->SizeContents.y; + + // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount) + // But we maintain a minimum size in pixel to allow for the user to still aim inside. + IM_ASSERT(ImMax(win_size_contents_v, win_size_avail_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers. + const float win_size_v = ImMax(ImMax(win_size_contents_v, win_size_avail_v), 1.0f); + const float grab_h_pixels = ImClamp(scrollbar_size_v * (win_size_avail_v / win_size_v), style.GrabMinSize, scrollbar_size_v); + const float grab_h_norm = grab_h_pixels / scrollbar_size_v; + + // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar(). + bool held = false; + bool hovered = false; + const bool previously_held = (g.ActiveId == id); + ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus); + + float scroll_max = ImMax(1.0f, win_size_contents_v - win_size_avail_v); + float scroll_ratio = ImSaturate(scroll_v / scroll_max); + float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; + if (held && grab_h_norm < 1.0f) + { + float scrollbar_pos_v = horizontal ? bb.Min.x : bb.Min.y; + float mouse_pos_v = horizontal ? g.IO.MousePos.x : g.IO.MousePos.y; + float* click_delta_to_grab_center_v = horizontal ? &g.ScrollbarClickDeltaToGrabCenter.x : &g.ScrollbarClickDeltaToGrabCenter.y; + + // Click position in scrollbar normalized space (0.0f->1.0f) + const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v); + SetHoveredID(id); + + bool seek_absolute = false; + if (!previously_held) + { + // On initial click calculate the distance between mouse and the center of the grab + if (clicked_v_norm >= grab_v_norm && clicked_v_norm <= grab_v_norm + grab_h_norm) + { + *click_delta_to_grab_center_v = clicked_v_norm - grab_v_norm - grab_h_norm*0.5f; + } + else + { + seek_absolute = true; + *click_delta_to_grab_center_v = 0.0f; + } + } + + // Apply scroll + // It is ok to modify Scroll here because we are being called in Begin() after the calculation of SizeContents and before setting up our starting position + const float scroll_v_norm = ImSaturate((clicked_v_norm - *click_delta_to_grab_center_v - grab_h_norm*0.5f) / (1.0f - grab_h_norm)); + scroll_v = (float)(int)(0.5f + scroll_v_norm * scroll_max);//(win_size_contents_v - win_size_v)); + if (horizontal) + window->Scroll.x = scroll_v; + else + window->Scroll.y = scroll_v; + + // Update values for rendering + scroll_ratio = ImSaturate(scroll_v / scroll_max); + grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; + + // Update distance to grab now that we have seeked and saturated + if (seek_absolute) + *click_delta_to_grab_center_v = clicked_v_norm - grab_v_norm - grab_h_norm*0.5f; + } + + // Render + const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab); + ImRect grab_rect; + if (horizontal) + grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImMin(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, window_rect.Max.x), bb.Max.y); + else + grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImMin(ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels, window_rect.Max.y)); + window->DrawList->AddRectFilled(grab_rect.Min, grab_rect.Max, grab_col, style.ScrollbarRounding); +} + +void ImGui::BringWindowToFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* current_front_window = g.Windows.back(); + if (current_front_window == window || current_front_window->RootWindow == window) + return; + for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the front most window + if (g.Windows[i] == window) + { + g.Windows.erase(g.Windows.Data + i); + g.Windows.push_back(window); + break; + } +} + +void ImGui::BringWindowToBack(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.Windows[0] == window) + return; + for (int i = 0; i < g.Windows.Size; i++) + if (g.Windows[i] == window) + { + memmove(&g.Windows[1], &g.Windows[0], (size_t)i * sizeof(ImGuiWindow*)); + g.Windows[0] = window; + break; + } +} + +// Moving window to front of display and set focus (which happens to be back of our sorted list) +void ImGui::FocusWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + if (g.NavWindow != window) + { + g.NavWindow = window; + if (window && g.NavDisableMouseHover) + g.NavMousePosDirty = true; + g.NavInitRequest = false; + g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId + g.NavIdIsAlive = false; + g.NavLayer = 0; + } + + // Passing NULL allow to disable keyboard focus + if (!window) + return; + + // Move the root window to the top of the pile + if (window->RootWindow) + window = window->RootWindow; + + // 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); +} + +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; + } +} + +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); +} + +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(); +} + +void ImGui::PopItemWidth() +{ + 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; +} + +static ImFont* GetDefaultFont() +{ + ImGuiContext& g = *GImGui; + return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; +} + +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; + + 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) +{ + ImGuiContext& g = *GImGui; + if (!font) + font = GetDefaultFont(); + SetCurrentFont(font); + g.FontStack.push_back(font); + g.CurrentWindow->DrawList->PushTextureID(font->ContainerAtlas->TexID); +} + +void ImGui::PopFont() +{ + 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); +} + +void ImGui::PopItemFlag() +{ + 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); +} + +void ImGui::PopAllowKeyboardFocus() +{ + PopItemFlag(); +} + +void ImGui::PushButtonRepeat(bool repeat) +{ + PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); +} + +void ImGui::PopButtonRepeat() +{ + 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); +} + +void ImGui::PopTextWrapPos() +{ + 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); +} + +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; +} + +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--; + } +} + +struct ImGuiStyleVarInfo +{ + 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 +}; + +static const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar 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. +} + +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. +} + +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; +} + +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; +} + +bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentWindow); // Not inside a Begin()/End() + + 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; + } +} + +// Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) +bool ImGui::IsWindowNavFocusable(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + return window->Active && window == window->RootWindowForTabbing && (!(window->Flags & ImGuiWindowFlags_NoNavFocus) || window == g.NavWindow); +} + +float ImGui::GetWindowWidth() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.x; +} + +float ImGui::GetWindowHeight() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->Size.y; +} + +ImVec2 ImGui::GetWindowPos() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + return window->Pos; +} + +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; +} + +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; +} + +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); + + // 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) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + SetWindowPos(window, pos, cond); +} + +void ImGui::SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowPos(window, pos, cond); +} + +ImVec2 ImGui::GetWindowSize() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Size; +} + +static void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowSizeAllowFlags & cond) == 0) + return; + window->SetWindowSizeAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + // 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) +{ + SetWindowSize(GImGui->CurrentWindow, size, cond); +} + +void ImGui::SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowSize(window, size, 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); + + // Set + window->Collapsed = collapsed; +} + +void ImGui::SetWindowCollapsed(bool collapsed, ImGuiCond cond) +{ + SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); +} + +bool ImGui::IsWindowCollapsed() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Collapsed; +} + +bool ImGui::IsWindowAppearing() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->Appearing; +} + +void ImGui::SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond) +{ + if (ImGuiWindow* window = FindWindowByName(name)) + SetWindowCollapsed(window, collapsed, cond); +} + +void ImGui::SetWindowFocus() +{ + FocusWindow(GImGui->CurrentWindow); +} + +void ImGui::SetWindowFocus(const char* name) +{ + if (name) + { + if (ImGuiWindow* window = FindWindowByName(name)) + FocusWindow(window); + } + else + { + FocusWindow(NULL); + } +} + +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; +} + +void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) +{ + 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) +{ + 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) +{ + 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; +} + +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) +} + +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) +} + +// 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; +} + +ImVec2 ImGui::GetContentRegionAvail() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return GetContentRegionMax() - (window->DC.CursorPos - window->Pos); +} + +float ImGui::GetContentRegionAvailWidth() +{ + return GetContentRegionAvail().x; +} + +// In window space (not screen space!) +ImVec2 ImGui::GetWindowContentRegionMin() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Min; +} + +ImVec2 ImGui::GetWindowContentRegionMax() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Max; +} + +float ImGui::GetWindowContentRegionWidth() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->ContentsRegionRect.Max.x - window->ContentsRegionRect.Min.x; +} + +float ImGui::GetTextLineHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize; +} + +float ImGui::GetTextLineHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.ItemSpacing.y; +} + +float ImGui::GetFrameHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f; +} + +float ImGui::GetFrameHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; +} + +ImDrawList* ImGui::GetWindowDrawList() +{ + ImGuiWindow* window = GetCurrentWindow(); + return window->DrawList; +} + +ImFont* ImGui::GetFont() +{ + return GImGui->Font; +} + +float ImGui::GetFontSize() +{ + return GImGui->FontSize; +} + +ImVec2 ImGui::GetFontTexUvWhitePixel() +{ + 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(); +} + +// User generally sees positions in window coordinates. Internally we store CursorPos in absolute screen coordinates because it is more convenient. +// Conversion happens as we pass the value to user, but it makes our naming convention confusing because GetCursorPos() == (DC.CursorPos - window.Pos). May want to rename 'DC.CursorPos'. +ImVec2 ImGui::GetCursorPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos - window->Pos + window->Scroll; +} + +float ImGui::GetCursorPosX() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.x - window->Pos.x + window->Scroll.x; +} + +float ImGui::GetCursorPosY() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos.y - window->Pos.y + window->Scroll.y; +} + +void ImGui::SetCursorPos(const ImVec2& local_pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = window->Pos - window->Scroll + local_pos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); +} + +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); +} + +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); +} + +ImVec2 ImGui::GetCursorStartPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorStartPos - window->Pos; +} + +ImVec2 ImGui::GetCursorScreenPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos; +} + +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); +} + +float ImGui::GetScrollX() +{ + return GImGui->CurrentWindow->Scroll.x; +} + +float ImGui::GetScrollY() +{ + return GImGui->CurrentWindow->Scroll.y; +} + +float ImGui::GetScrollMaxX() +{ + return GetScrollMaxX(GImGui->CurrentWindow); +} + +float ImGui::GetScrollMaxY() +{ + return GetScrollMaxY(GImGui->CurrentWindow); +} + +void ImGui::SetScrollX(float scroll_x) +{ + 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; +} + +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; + + // 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); +} + +void ImGui::ActivateItem(ImGuiID 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; +} + +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; +} + +void ImGui::TextV(const char* fmt, va_list args) +{ + 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); +} + +void ImGui::Text(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); +} + +void ImGui::TextColoredV(const ImVec4& col, const char* fmt, va_list args) +{ + PushStyleColor(ImGuiCol_Text, col); + TextV(fmt, args); + PopStyleColor(); +} + +void ImGui::TextColored(const ImVec4& col, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + TextColoredV(col, fmt, args); + va_end(args); +} + +void ImGui::TextDisabledV(const char* fmt, va_list args) +{ + PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]); + TextV(fmt, args); + PopStyleColor(); +} + +void ImGui::TextDisabled(const char* fmt, ...) +{ + 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; + + ImGuiContext& g = *GImGui; + window->DC.CurrentLineHeight = ImMax(window->DC.CurrentLineHeight, g.FontSize + g.Style.FramePadding.y * 2); + window->DC.CurrentLineTextBaseOffset = ImMax(window->DC.CurrentLineTextBaseOffset, g.Style.FramePadding.y); +} + +// Add a label+text combo aligned to other label+value widgets +void ImGui::LabelTextV(const char* label, const char* fmt, va_list args) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect value_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2)); + const ImRect total_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x : 0.0f), style.FramePadding.y*2) + label_size); + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, 0)) + return; + + // Render + const char* value_text_begin = &g.TempBuffer[0]; + const char* value_text_end = value_text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + RenderTextClipped(value_bb.Min, value_bb.Max, value_text_begin, value_text_end, NULL, ImVec2(0.0f,0.5f)); + if (label_size.x > 0.0f) + RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); +} + +void ImGui::LabelText(const char* label, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + LabelTextV(label, fmt, args); + va_end(args); +} + +bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + if (flags & ImGuiButtonFlags_Disabled) + { + if (out_hovered) *out_hovered = false; + if (out_held) *out_held = false; + if (g.ActiveId == id) ClearActiveID(); + return false; + } + + // Default behavior requires click+release on same spot + if ((flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick)) == 0) + flags |= ImGuiButtonFlags_PressedOnClickRelease; + + ImGuiWindow* backup_hovered_window = g.HoveredWindow; + if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window) + g.HoveredWindow = window; + + bool pressed = false; + bool hovered = ItemHoverable(bb, id); + + // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button + if ((flags & ImGuiButtonFlags_PressedOnDragDropHold) && g.DragDropActive && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) + if (IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + { + hovered = true; + SetHoveredID(id); + if (CalcTypematicPressedRepeatAmount(g.HoveredIdTimer + 0.0001f, g.HoveredIdTimer + 0.0001f - g.IO.DeltaTime, 0.01f, 0.70f)) // FIXME: Our formula for CalcTypematicPressedRepeatAmount() is fishy + { + pressed = true; + FocusWindow(window); + } + } + + if ((flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window) + g.HoveredWindow = backup_hovered_window; + + // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one. + if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0)) + hovered = false; + + // Mouse + if (hovered) + { + if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt)) + { + // | CLICKING | HOLDING with ImGuiButtonFlags_Repeat + // PressedOnClickRelease | * | .. (NOT on release) <-- MOST COMMON! (*) only if both click/release were over bounds + // PressedOnClick | | .. + // PressedOnRelease | | .. (NOT on release) + // PressedOnDoubleClick | | .. + // FIXME-NAV: We don't honor those different behaviors. + if ((flags & ImGuiButtonFlags_PressedOnClickRelease) && g.IO.MouseClicked[0]) + { + SetActiveID(id, window); + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); + FocusWindow(window); + } + if (((flags & ImGuiButtonFlags_PressedOnClick) && g.IO.MouseClicked[0]) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDoubleClicked[0])) + { + pressed = true; + if (flags & ImGuiButtonFlags_NoHoldingActiveID) + ClearActiveID(); + else + SetActiveID(id, window); // Hold on ID + FocusWindow(window); + } + if ((flags & ImGuiButtonFlags_PressedOnRelease) && g.IO.MouseReleased[0]) + { + if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps + pressed = true; + ClearActiveID(); + } + + // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). + // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. + if ((flags & ImGuiButtonFlags_Repeat) && g.ActiveId == id && g.IO.MouseDownDuration[0] > 0.0f && IsMouseClicked(0, true)) + pressed = true; + } + + if (pressed) + g.NavDisableHighlight = true; + } + + // Gamepad/Keyboard navigation + // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. + if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) + hovered = true; + + if (g.NavActivateDownId == id) + { + bool nav_activated_by_code = (g.NavActivateId == id); + bool nav_activated_by_inputs = IsNavInputPressed(ImGuiNavInput_Activate, (flags & ImGuiButtonFlags_Repeat) ? ImGuiInputReadMode_Repeat : ImGuiInputReadMode_Pressed); + if (nav_activated_by_code || nav_activated_by_inputs) + pressed = true; + if (nav_activated_by_code || nav_activated_by_inputs || g.ActiveId == id) + { + // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button. + g.NavActivateId = id; // This is so SetActiveId assign a Nav source + SetActiveID(id, window); + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); + g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right) | (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + } + } + + bool held = false; + if (g.ActiveId == id) + { + if (g.ActiveIdSource == ImGuiInputSource_Mouse) + { + if (g.ActiveIdIsJustActivated) + g.ActiveIdClickOffset = g.IO.MousePos - bb.Min; + if (g.IO.MouseDown[0]) + { + held = true; + } + else + { + if (hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease)) + if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps + if (!g.DragDropActive) + pressed = true; + ClearActiveID(); + } + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + g.NavDisableHighlight = true; + } + else if (g.ActiveIdSource == ImGuiInputSource_Nav) + { + if (g.NavActivateDownId != id) + ClearActiveID(); + } + } + + if (out_hovered) *out_hovered = hovered; + if (out_held) *out_held = held; + + return pressed; +} + +bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + + ImVec2 pos = window->DC.CursorPos; + if ((flags & ImGuiButtonFlags_AlignTextBaseLine) && style.FramePadding.y < window->DC.CurrentLineTextBaseOffset) // Try to vertically align buttons that are smaller/have no padding so that text baseline matches (bit hacky, since it shouldn't be a flag) + pos.y += window->DC.CurrentLineTextBaseOffset - style.FramePadding.y; + ImVec2 size = CalcItemSize(size_arg, label_size.x + style.FramePadding.x * 2.0f, label_size.y + style.FramePadding.y * 2.0f); + + const ImRect bb(pos, pos + size); + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(bb, id)) + return false; + + if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat) + flags |= ImGuiButtonFlags_Repeat; + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + + // Render + const ImU32 col = GetColorU32((hovered && held) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); + + // Automatically close popups + //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) + // CloseCurrentPopup(); + + return pressed; +} + +bool ImGui::Button(const char* label, const ImVec2& size_arg) +{ + return ButtonEx(label, size_arg, 0); +} + +// Small buttons fits within text without additional vertical spacing. +bool ImGui::SmallButton(const char* label) +{ + ImGuiContext& g = *GImGui; + float backup_padding_y = g.Style.FramePadding.y; + g.Style.FramePadding.y = 0.0f; + bool pressed = ButtonEx(label, ImVec2(0,0), ImGuiButtonFlags_AlignTextBaseLine); + g.Style.FramePadding.y = backup_padding_y; + return pressed; +} + +// Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack. +// Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id) +bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + const ImGuiID id = window->GetID(str_id); + ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + return pressed; +} + +// Button to close a window +bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos, float radius) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // 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; + + // 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; +} + +// [Internal] +bool ImGui::ArrowButton(ImGuiID id, ImGuiDir dir, ImVec2 padding, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + 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; + + 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); + + 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) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + if (border_col.w > 0.0f) + bb.Max += ImVec2(2,2); + ItemSize(bb); + if (!ItemAdd(bb, 0)) + return; + + if (border_col.w > 0.0f) + { + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f); + window->DrawList->AddImage(user_texture_id, bb.Min+ImVec2(1,1), bb.Max-ImVec2(1,1), uv0, uv1, GetColorU32(tint_col)); + } + else + { + window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col)); + } +} + +// frame_padding < 0: uses FramePadding from style (default) +// frame_padding = 0: no framing +// frame_padding > 0: set framing size +// The color used are the button colors. +bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + + // Default to using texture ID as ID. User can still push string/integer prefixes. + // We could hash the size/uv to create a unique ID but that would prevent the user from animating UV. + PushID((void *)user_texture_id); + const ImGuiID id = window->GetID("#image"); + PopID(); + + const ImVec2 padding = (frame_padding >= 0) ? ImVec2((float)frame_padding, (float)frame_padding) : style.FramePadding; + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding*2); + const ImRect image_bb(window->DC.CursorPos + padding, window->DC.CursorPos + padding + size); + ItemSize(bb); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + // Render + const ImU32 col = GetColorU32((hovered && held) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + RenderNavHighlight(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, style.FrameRounding)); + if (bg_col.w > 0.0f) + window->DrawList->AddRectFilled(image_bb.Min, image_bb.Max, GetColorU32(bg_col)); + window->DrawList->AddImage(user_texture_id, image_bb.Min, image_bb.Max, uv0, uv1, GetColorU32(tint_col)); + + return pressed; +} + +// Start logging ImGui output to TTY +void ImGui::LogToTTY(int max_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + ImGuiWindow* window = g.CurrentWindow; + + IM_ASSERT(g.LogFile == NULL); + g.LogFile = stdout; + g.LogEnabled = true; + g.LogStartDepth = window->DC.TreeDepth; + if (max_depth >= 0) + g.LogAutoExpandMaxDepth = max_depth; +} + +// Start logging ImGui output to given file +void ImGui::LogToFile(int max_depth, const char* filename) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + ImGuiWindow* window = g.CurrentWindow; + + if (!filename) + { + filename = g.IO.LogFilename; + if (!filename) + return; + } + + IM_ASSERT(g.LogFile == NULL); + g.LogFile = ImFileOpen(filename, "ab"); + if (!g.LogFile) + { + IM_ASSERT(g.LogFile != NULL); // Consider this an error + return; + } + g.LogEnabled = true; + g.LogStartDepth = window->DC.TreeDepth; + if (max_depth >= 0) + g.LogAutoExpandMaxDepth = max_depth; +} + +// Start logging ImGui output to clipboard +void ImGui::LogToClipboard(int max_depth) +{ + ImGuiContext& g = *GImGui; + if (g.LogEnabled) + return; + ImGuiWindow* window = g.CurrentWindow; + + IM_ASSERT(g.LogFile == NULL); + g.LogFile = NULL; + g.LogEnabled = true; + g.LogStartDepth = window->DC.TreeDepth; + if (max_depth >= 0) + g.LogAutoExpandMaxDepth = max_depth; +} + +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; +} + +// Helper to display logging buttons +void ImGui::LogButtons() +{ + ImGuiContext& g = *GImGui; + + PushID("LogButtons"); + const bool log_to_tty = Button("Log To TTY"); SameLine(); + const bool log_to_file = Button("Log To File"); SameLine(); + const bool log_to_clipboard = Button("Log To Clipboard"); SameLine(); + PushItemWidth(80.0f); + PushAllowKeyboardFocus(false); + SliderInt("Depth", &g.LogAutoExpandMaxDepth, 0, 9, NULL); + PopAllowKeyboardFocus(); + PopItemWidth(); + PopID(); + + // Start logging at the end of the function so that the buttons don't appear in the log + if (log_to_tty) + LogToTTY(g.LogAutoExpandMaxDepth); + if (log_to_file) + LogToFile(g.LogAutoExpandMaxDepth, g.IO.LogFilename); + if (log_to_clipboard) + LogToClipboard(g.LogAutoExpandMaxDepth); +} + +bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags) +{ + if (flags & ImGuiTreeNodeFlags_Leaf) + return true; + + // We only write to the tree storage if the user clicks (or explicitely use SetNextTreeNode*** functions) + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStorage* storage = window->DC.StateStorage; + + bool is_open; + if (g.NextTreeNodeOpenCond != 0) + { + if (g.NextTreeNodeOpenCond & ImGuiCond_Always) + { + is_open = g.NextTreeNodeOpenVal; + storage->SetInt(id, is_open); + } + else + { + // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently. + const int stored_value = storage->GetInt(id, -1); + if (stored_value == -1) + { + is_open = g.NextTreeNodeOpenVal; + storage->SetInt(id, is_open); + } + else + { + is_open = stored_value != 0; + } + } + g.NextTreeNodeOpenCond = 0; + } + else + { + is_open = storage->GetInt(id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; + } + + // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior). + // NB- If we are above max depth we still allow manually opened nodes to be logged. + if (g.LogEnabled && !(flags & ImGuiTreeNodeFlags_NoAutoOpenOnLog) && window->DC.TreeDepth < g.LogAutoExpandMaxDepth) + is_open = true; + + return is_open; +} + +bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; + const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, 0.0f); + + if (!label_end) + label_end = FindRenderedTextEnd(label); + const ImVec2 label_size = CalcTextSize(label, label_end, false); + + // We vertically grow up to current line height up the typical widget height. + const float text_base_offset_y = ImMax(padding.y, window->DC.CurrentLineTextBaseOffset); // Latch before ItemSize changes it + const float frame_height = ImMax(ImMin(window->DC.CurrentLineHeight, g.FontSize + style.FramePadding.y*2), label_size.y + padding.y*2); + ImRect frame_bb = ImRect(window->DC.CursorPos, ImVec2(window->Pos.x + GetContentRegionMax().x, window->DC.CursorPos.y + frame_height)); + if (display_frame) + { + // Framed header expand a little outside the default padding + frame_bb.Min.x -= (float)(int)(window->WindowPadding.x*0.5f) - 1; + frame_bb.Max.x += (float)(int)(window->WindowPadding.x*0.5f) - 1; + } + + const float text_offset_x = (g.FontSize + (display_frame ? padding.x*3 : padding.x*2)); // Collapser arrow width + Spacing + const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x*2 : 0.0f); // Include collapser + ItemSize(ImVec2(text_width, frame_height), text_base_offset_y); + + // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing + // (Ideally we'd want to add a flag for the user to specify if we want the hit test to be done up to the right side of the content or not) + const ImRect interact_bb = display_frame ? frame_bb : ImRect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + text_width + style.ItemSpacing.x*2, frame_bb.Max.y); + bool is_open = TreeNodeBehaviorIsOpen(id, flags); + + // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child. + // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). + // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero. + if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + window->DC.TreeDepthMayJumpToParentOnPop |= (1 << window->DC.TreeDepth); + + bool item_add = ItemAdd(interact_bb, id); + window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; + window->DC.LastItemDisplayRect = frame_bb; + + if (!item_add) + { + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushRawID(id); + return is_open; + } + + // Flags that affects opening behavior: + // - 0(default) ..................... single-click anywhere to open + // - OpenOnDoubleClick .............. double-click anywhere to open + // - OpenOnArrow .................... single-click on arrow to open + // - OpenOnDoubleClick|OpenOnArrow .. single-click on arrow or double-click anywhere to open + ImGuiButtonFlags button_flags = ImGuiButtonFlags_NoKeyModifiers | ((flags & ImGuiTreeNodeFlags_AllowItemOverlap) ? ImGuiButtonFlags_AllowItemOverlap : 0); + if (!(flags & ImGuiTreeNodeFlags_Leaf)) + button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) + button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0); + + bool hovered, held, pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); + if (!(flags & ImGuiTreeNodeFlags_Leaf)) + { + bool toggled = false; + if (pressed) + { + toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id); + if (flags & ImGuiTreeNodeFlags_OpenOnArrow) + toggled |= IsMouseHoveringRect(interact_bb.Min, ImVec2(interact_bb.Min.x + text_offset_x, interact_bb.Max.y)) && (!g.NavDisableMouseHover); + if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) + toggled |= g.IO.MouseDoubleClicked[0]; + if (g.DragDropActive && is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. + toggled = false; + } + + if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Left && is_open) + { + toggled = true; + NavMoveRequestCancel(); + } + if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? + { + toggled = true; + NavMoveRequestCancel(); + } + + if (toggled) + { + is_open = !is_open; + window->DC.StateStorage->SetInt(id, is_open); + } + } + if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) + SetItemAllowOverlap(); + + // Render + const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + const ImVec2 text_pos = frame_bb.Min + ImVec2(text_offset_x, text_base_offset_y); + if (display_frame) + { + // Framed type + RenderFrame(frame_bb.Min, frame_bb.Max, col, true, style.FrameRounding); + RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); + RenderTriangle(frame_bb.Min + ImVec2(padding.x, text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); + if (g.LogEnabled) + { + // NB: '##' is normally used to hide text (as a library-wide feature), so we need to specify the text range to make sure the ## aren't stripped out here. + const char log_prefix[] = "\n##"; + const char log_suffix[] = "##"; + LogRenderedText(&text_pos, log_prefix, log_prefix+3); + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + LogRenderedText(&text_pos, log_suffix+1, log_suffix+3); + } + else + { + RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); + } + } + else + { + // Unframed typed for tree nodes + if (hovered || (flags & ImGuiTreeNodeFlags_Selected)) + { + RenderFrame(frame_bb.Min, frame_bb.Max, col, false); + RenderNavHighlight(frame_bb, id, ImGuiNavHighlightFlags_TypeThin); + } + + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(frame_bb.Min + ImVec2(text_offset_x * 0.5f, g.FontSize*0.50f + text_base_offset_y)); + else if (!(flags & ImGuiTreeNodeFlags_Leaf)) + RenderTriangle(frame_bb.Min + ImVec2(padding.x, g.FontSize*0.15f + text_base_offset_y), is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f); + if (g.LogEnabled) + LogRenderedText(&text_pos, ">"); + RenderText(text_pos, label, label_end, false); + } + + if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + TreePushRawID(id); + return is_open; +} + +// CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag). +// This is basically the same as calling TreeNodeEx(label, ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen). You can remove the _NoTreePushOnOpen flag if you want behavior closer to normal TreeNode(). +bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen, label); +} + +bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + if (p_open && !*p_open) + return false; + + ImGuiID id = window->GetID(label); + bool is_open = TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_NoTreePushOnOpen | (p_open ? ImGuiTreeNodeFlags_AllowItemOverlap : 0), label); + if (p_open) + { + // Create a small overlapping close button // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc. + ImGuiContext& g = *GImGui; + float button_sz = g.FontSize * 0.5f; + ImGuiItemHoveredDataBackup last_item_backup; + if (CloseButton(window->GetID((void*)(intptr_t)(id+1)), ImVec2(ImMin(window->DC.LastItemRect.Max.x, window->ClipRect.Max.x) - g.Style.FramePadding.x - button_sz, window->DC.LastItemRect.Min.y + g.Style.FramePadding.y + button_sz), button_sz)) + *p_open = false; + last_item_backup.Restore(); + } + + return is_open; +} + +bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + return TreeNodeBehavior(window->GetID(label), flags, label, NULL); +} + +bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +{ + 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); +} + +bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) +{ + 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); +} + +bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args) +{ + return TreeNodeExV(str_id, 0, fmt, args); +} + +bool ImGui::TreeNodeV(const void* ptr_id, const char* fmt, va_list args) +{ + return TreeNodeExV(ptr_id, 0, fmt, args); +} + +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; +} + +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; +} + +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; +} + +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; +} + +bool ImGui::TreeNode(const char* label) +{ + 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(); +} + +// Horizontal distance preceding label when using TreeNode() or Bullet() +float ImGui::GetTreeNodeToLabelSpacing() +{ + 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; +} + +void ImGui::PushID(const char* 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) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + window->IDStack.push_back(window->GetID(str_id_begin, str_id_end)); +} + +void ImGui::PushID(const void* 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)); +} + +void ImGui::PopID() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + window->IDStack.pop_back(); +} + +ImGuiID ImGui::GetID(const char* str_id) +{ + return GImGui->CurrentWindow->GetID(str_id); +} + +ImGuiID ImGui::GetID(const char* str_id_begin, const char* str_id_end) +{ + return GImGui->CurrentWindow->GetID(str_id_begin, str_id_end); +} + +ImGuiID ImGui::GetID(const void* ptr_id) +{ + return GImGui->CurrentWindow->GetID(ptr_id); +} + +void ImGui::Bullet() +{ + 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; + } + + // 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; + } +} + +// User can input math operators (e.g. +100) to edit a numerical values. +static bool DataTypeApplyOpFromText(const char* buf, const char* initial_value_buf, ImGuiDataType data_type, void* data_ptr, const char* scalar_format) +{ + while (ImCharIsSpace(*buf)) + buf++; + + // We don't support '-' op because it would conflict with inputing negative value. + // Instead you can use +-100 to subtract from an existing value + char op = buf[0]; + if (op == '+' || op == '*' || op == '/') + { + buf++; + while (ImCharIsSpace(*buf)) + buf++; + } + else + { + op = 0; + } + if (!buf[0]) + return false; + + if (data_type == ImGuiDataType_Int) + { + if (!scalar_format) + scalar_format = "%d"; + int* v = (int*)data_ptr; + const int old_v = *v; + int arg0i = *v; + if (op && sscanf(initial_value_buf, scalar_format, &arg0i) < 1) + return false; + + // Store operand in a float so we can use fractional value for multipliers (*1.1), but constant always parsed as integer so we can fit big integers (e.g. 2000000003) past float precision + float arg1f = 0.0f; + if (op == '+') { if (sscanf(buf, "%f", &arg1f) == 1) *v = (int)(arg0i + arg1f); } // Add (use "+-" to subtract) + else if (op == '*') { if (sscanf(buf, "%f", &arg1f) == 1) *v = (int)(arg0i * arg1f); } // Multiply + else if (op == '/') { if (sscanf(buf, "%f", &arg1f) == 1 && arg1f != 0.0f) *v = (int)(arg0i / arg1f); }// Divide + else { if (sscanf(buf, scalar_format, &arg0i) == 1) *v = arg0i; } // Assign constant (read as integer so big values are not lossy) + return (old_v != *v); + } + else if (data_type == ImGuiDataType_Float) + { + // For floats we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in + scalar_format = "%f"; + float* v = (float*)data_ptr; + const float old_v = *v; + float arg0f = *v; + if (op && sscanf(initial_value_buf, scalar_format, &arg0f) < 1) + return false; + + float arg1f = 0.0f; + if (sscanf(buf, scalar_format, &arg1f) < 1) + return false; + if (op == '+') { *v = arg0f + arg1f; } // Add (use "+-" to subtract) + else if (op == '*') { *v = arg0f * arg1f; } // Multiply + else if (op == '/') { if (arg1f != 0.0f) *v = arg0f / arg1f; } // Divide + else { *v = arg1f; } // Assign constant + return (old_v != *v); + } + + return false; +} + +// Create text input in place of a slider (when CTRL+Clicking on slider) +// FIXME: Logic is messy and confusing. +bool ImGui::InputScalarAsWidgetReplacement(const ImRect& aabb, const char* label, ImGuiDataType data_type, void* data_ptr, ImGuiID id, int decimal_precision) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + // Our replacement widget will override the focus ID (registered previously to allow for a TAB focus to happen) + // On the first frame, g.ScalarAsInputTextId == 0, then on subsequent frames it becomes == id + SetActiveID(g.ScalarAsInputTextId, window); + g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + SetHoveredID(0); + FocusableItemUnregister(window); + + char buf[32]; + DataTypeFormatString(data_type, data_ptr, decimal_precision, buf, IM_ARRAYSIZE(buf)); + bool text_value_changed = InputTextEx(label, buf, IM_ARRAYSIZE(buf), aabb.GetSize(), ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_AutoSelectAll); + if (g.ScalarAsInputTextId == 0) // First frame we started displaying the InputText widget + { + IM_ASSERT(g.ActiveId == id); // InputText ID expected to match the Slider ID (else we'd need to store them both, which is also possible) + g.ScalarAsInputTextId = g.ActiveId; + SetHoveredID(id); + } + if (text_value_changed) + return DataTypeApplyOpFromText(buf, GImGui->InputTextState.InitialText.begin(), data_type, data_ptr, NULL); + return false; +} + +// Parse display precision back from the display format string +int ImGui::ParseFormatPrecision(const char* fmt, int default_precision) +{ + int precision = default_precision; + while ((fmt = strchr(fmt, '%')) != NULL) + { + fmt++; + if (fmt[0] == '%') { fmt++; continue; } // Ignore "%%" + while (*fmt >= '0' && *fmt <= '9') + fmt++; + if (*fmt == '.') + { + fmt = ImAtoi(fmt + 1, &precision); + if (precision < 0 || precision > 10) + precision = default_precision; + } + if (*fmt == 'e' || *fmt == 'E') // Maximum precision with scientific notation + precision = -1; + break; + } + return precision; +} + +static float GetMinimumStepAtDecimalPrecision(int decimal_precision) +{ + static const float min_steps[10] = { 1.0f, 0.1f, 0.01f, 0.001f, 0.0001f, 0.00001f, 0.000001f, 0.0000001f, 0.00000001f, 0.000000001f }; + return (decimal_precision >= 0 && decimal_precision < 10) ? min_steps[decimal_precision] : powf(10.0f, (float)-decimal_precision); +} + +float ImGui::RoundScalar(float value, int decimal_precision) +{ + // Round past decimal precision + // So when our value is 1.99999 with a precision of 0.001 we'll end up rounding to 2.0 + // FIXME: Investigate better rounding methods + if (decimal_precision < 0) + return value; + const float min_step = GetMinimumStepAtDecimalPrecision(decimal_precision); + bool negative = value < 0.0f; + value = fabsf(value); + float remainder = fmodf(value, min_step); + if (remainder <= min_step*0.5f) + value -= remainder; + else + value += (min_step - remainder); + return negative ? -value : value; +} + +static inline float SliderBehaviorCalcRatioFromValue(float v, float v_min, float v_max, float power, float linear_zero_pos) +{ + if (v_min == v_max) + return 0.0f; + + const bool is_non_linear = (power < 1.0f-0.00001f) || (power > 1.0f+0.00001f); + const float v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min); + if (is_non_linear) + { + if (v_clamped < 0.0f) + { + const float f = 1.0f - (v_clamped - v_min) / (ImMin(0.0f,v_max) - v_min); + return (1.0f - powf(f, 1.0f/power)) * linear_zero_pos; + } + else + { + const float f = (v_clamped - ImMax(0.0f,v_min)) / (v_max - ImMax(0.0f,v_min)); + return linear_zero_pos + powf(f, 1.0f/power) * (1.0f - linear_zero_pos); + } + } + + // Linear slider + return (v_clamped - v_min) / (v_max - v_min); +} + +bool ImGui::SliderBehavior(const ImRect& frame_bb, ImGuiID id, float* v, float v_min, float v_max, float power, int decimal_precision, ImGuiSliderFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + const ImGuiStyle& style = g.Style; + + // Draw frame + const ImU32 frame_col = GetColorU32((g.ActiveId == id && g.ActiveIdSource == ImGuiInputSource_Nav) ? ImGuiCol_FrameBgActive : ImGuiCol_FrameBg); + RenderNavHighlight(frame_bb, id); + RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); + + const bool is_non_linear = (power < 1.0f-0.00001f) || (power > 1.0f+0.00001f); + const bool is_horizontal = (flags & ImGuiSliderFlags_Vertical) == 0; + + const float grab_padding = 2.0f; + const float slider_sz = is_horizontal ? (frame_bb.GetWidth() - grab_padding * 2.0f) : (frame_bb.GetHeight() - grab_padding * 2.0f); + float grab_sz; + if (decimal_precision != 0) + grab_sz = ImMin(style.GrabMinSize, slider_sz); + else + grab_sz = ImMin(ImMax(1.0f * (slider_sz / ((v_min < v_max ? v_max - v_min : v_min - v_max) + 1.0f)), style.GrabMinSize), slider_sz); // Integer sliders, if possible have the grab size represent 1 unit + const float slider_usable_sz = slider_sz - grab_sz; + const float slider_usable_pos_min = (is_horizontal ? frame_bb.Min.x : frame_bb.Min.y) + grab_padding + grab_sz*0.5f; + const float slider_usable_pos_max = (is_horizontal ? frame_bb.Max.x : frame_bb.Max.y) - grab_padding - grab_sz*0.5f; + + // For logarithmic sliders that cross over sign boundary we want the exponential increase to be symmetric around 0.0f + float linear_zero_pos = 0.0f; // 0.0->1.0f + if (v_min * v_max < 0.0f) + { + // Different sign + const float linear_dist_min_to_0 = powf(fabsf(0.0f - v_min), 1.0f/power); + const float linear_dist_max_to_0 = powf(fabsf(v_max - 0.0f), 1.0f/power); + linear_zero_pos = linear_dist_min_to_0 / (linear_dist_min_to_0+linear_dist_max_to_0); + } + else + { + // Same sign + linear_zero_pos = v_min < 0.0f ? 1.0f : 0.0f; + } + + // Process interacting with the slider + bool value_changed = false; + if (g.ActiveId == id) + { + bool set_new_value = false; + float clicked_t = 0.0f; + if (g.ActiveIdSource == ImGuiInputSource_Mouse) + { + if (!g.IO.MouseDown[0]) + { + ClearActiveID(); + } + else + { + const float mouse_abs_pos = is_horizontal ? g.IO.MousePos.x : g.IO.MousePos.y; + clicked_t = (slider_usable_sz > 0.0f) ? ImClamp((mouse_abs_pos - slider_usable_pos_min) / slider_usable_sz, 0.0f, 1.0f) : 0.0f; + if (!is_horizontal) + clicked_t = 1.0f - clicked_t; + set_new_value = true; + } + } + else if (g.ActiveIdSource == ImGuiInputSource_Nav) + { + const ImVec2 delta2 = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 0.0f, 0.0f); + float delta = is_horizontal ? delta2.x : -delta2.y; + if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) + { + ClearActiveID(); + } + else if (delta != 0.0f) + { + clicked_t = SliderBehaviorCalcRatioFromValue(*v, v_min, v_max, power, linear_zero_pos); + if (decimal_precision == 0 && !is_non_linear) + { + if (fabsf(v_max - v_min) <= 100.0f || IsNavInputDown(ImGuiNavInput_TweakSlow)) + delta = ((delta < 0.0f) ? -1.0f : +1.0f) / (v_max - v_min); // Gamepad/keyboard tweak speeds in integer steps + else + delta /= 100.0f; + } + else + { + delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds + if (IsNavInputDown(ImGuiNavInput_TweakSlow)) + delta /= 10.0f; + } + if (IsNavInputDown(ImGuiNavInput_TweakFast)) + delta *= 10.0f; + set_new_value = true; + if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits + set_new_value = false; + else + clicked_t = ImSaturate(clicked_t + delta); + } + } + + if (set_new_value) + { + float new_value; + if (is_non_linear) + { + // Account for logarithmic scale on both sides of the zero + if (clicked_t < linear_zero_pos) + { + // Negative: rescale to the negative range before powering + float a = 1.0f - (clicked_t / linear_zero_pos); + a = powf(a, power); + new_value = ImLerp(ImMin(v_max,0.0f), v_min, a); + } + else + { + // Positive: rescale to the positive range before powering + float a; + if (fabsf(linear_zero_pos - 1.0f) > 1.e-6f) + a = (clicked_t - linear_zero_pos) / (1.0f - linear_zero_pos); + else + a = clicked_t; + a = powf(a, power); + new_value = ImLerp(ImMax(v_min,0.0f), v_max, a); + } + } + else + { + // Linear slider + new_value = ImLerp(v_min, v_max, clicked_t); + } + + // Round past decimal precision + new_value = RoundScalar(new_value, decimal_precision); + if (*v != new_value) + { + *v = new_value; + value_changed = true; + } + } + } + + // Draw + float grab_t = SliderBehaviorCalcRatioFromValue(*v, v_min, v_max, power, linear_zero_pos); + if (!is_horizontal) + grab_t = 1.0f - grab_t; + const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); + ImRect grab_bb; + if (is_horizontal) + grab_bb = ImRect(ImVec2(grab_pos - grab_sz*0.5f, frame_bb.Min.y + grab_padding), ImVec2(grab_pos + grab_sz*0.5f, frame_bb.Max.y - grab_padding)); + else + grab_bb = ImRect(ImVec2(frame_bb.Min.x + grab_padding, grab_pos - grab_sz*0.5f), ImVec2(frame_bb.Max.x - grab_padding, grab_pos + grab_sz*0.5f)); + window->DrawList->AddRectFilled(grab_bb.Min, grab_bb.Max, GetColorU32(g.ActiveId == id ? ImGuiCol_SliderGrabActive : ImGuiCol_SliderGrab), style.GrabRounding); + + return value_changed; +} + +// Use power!=1.0 for logarithmic sliders. +// Adjust display_format to decorate the value with a prefix or a suffix. +// "%.3f" 1.234 +// "%5.2f secs" 01.23 secs +// "Gold: %.0f" Gold: 1 +bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* display_format, float power) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + const float w = CalcItemWidth(); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f)); + const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + // NB- we don't call ItemSize() yet because we may turn into a text edit box below + if (!ItemAdd(total_bb, id, &frame_bb)) + { + ItemSize(total_bb, style.FramePadding.y); + return false; + } + const bool hovered = ItemHoverable(frame_bb, id); + + if (!display_format) + display_format = "%.3f"; + int decimal_precision = ParseFormatPrecision(display_format, 3); + + // Tabbing or CTRL-clicking on Slider turns it into an input box + bool start_text_input = false; + const bool tab_focus_requested = FocusableItemRegister(window, id); + if (tab_focus_requested || (hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || (g.NavInputId == id && g.ScalarAsInputTextId != id)) + { + SetActiveID(id, window); + SetFocusID(id, window); + FocusWindow(window); + g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + if (tab_focus_requested || g.IO.KeyCtrl || g.NavInputId == id) + { + start_text_input = true; + g.ScalarAsInputTextId = 0; + } + } + if (start_text_input || (g.ActiveId == id && g.ScalarAsInputTextId == id)) + return InputScalarAsWidgetReplacement(frame_bb, label, ImGuiDataType_Float, v, id, decimal_precision); + + // Actual slider behavior + render grab + ItemSize(total_bb, style.FramePadding.y); + const bool value_changed = SliderBehavior(frame_bb, id, v, v_min, v_max, power, decimal_precision); + + // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. + char value_buf[64]; + const char* value_buf_end = value_buf + ImFormatString(value_buf, IM_ARRAYSIZE(value_buf), display_format, *v); + RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f,0.5f)); + + if (label_size.x > 0.0f) + RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + + return value_changed; +} + +bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* display_format, float power) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const ImGuiID id = window->GetID(label); + + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); + const ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(frame_bb, id)) + return false; + const bool hovered = ItemHoverable(frame_bb, id); + + if (!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) +{ + 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; +} + +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; +} + +// 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(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +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); +} + +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); +} + +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); +} + +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; + + 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); +} + +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(); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + + return value_changed; +} + +bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* display_format, float power) +{ + return DragFloatN(label, v, 2, v_speed, v_min, v_max, display_format, power); +} + +bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* display_format, float power) +{ + return DragFloatN(label, v, 3, v_speed, v_min, v_max, display_format, power); +} + +bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* display_format, float power) +{ + return DragFloatN(label, v, 4, v_speed, v_min, v_max, display_format, power); +} + +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; + + ImGuiContext& g = *GImGui; + PushID(label); + BeginGroup(); + PushMultiItemsWidths(2); + + bool value_changed = DragFloat("##min", v_current_min, v_speed, (v_min >= v_max) ? -FLT_MAX : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), display_format, power); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + value_changed |= DragFloat("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? FLT_MAX : v_max, display_format_max ? display_format_max : display_format, power); + PopItemWidth(); + SameLine(0, g.Style.ItemInnerSpacing.x); + + TextUnformatted(label, FindRenderedTextEnd(label)); + EndGroup(); + PopID(); + + return value_changed; +} + +// 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) +{ + 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) +{ + 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; +} + +bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* display_format) +{ + return DragIntN(label, v, 2, v_speed, v_min, v_max, 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 DragIntN(label, v, 3, v_speed, v_min, v_max, 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 DragIntN(label, v, 4, v_speed, v_min, v_max, display_format); +} + +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) +{ + 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); +} + +struct ImGuiPlotArrayGetterData +{ + const float* Values; + int Stride; + + ImGuiPlotArrayGetterData(const float* values, int stride) { Values = values; Stride = stride; } +}; + +static float Plot_ArrayGetter(void* data, 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; +} + +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) +{ + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Lines, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +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) +{ + PlotEx(ImGuiPlotType_Lines, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +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) +{ + ImGuiPlotArrayGetterData data(values, stride); + PlotEx(ImGuiPlotType_Histogram, label, &Plot_ArrayGetter, (void*)&data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +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) +{ + PlotEx(ImGuiPlotType_Histogram, label, values_getter, data, values_count, values_offset, overlay_text, scale_min, scale_max, graph_size); +} + +// 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; +} + +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; + + 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) + } + + return menu_is_open; +} + +void ImGui::EndMenu() +{ + // Nav: When a left move request _within our child menu_ failed, close the menu. + // A menu doesn't close itself because EndMenuBar() wants the catch the last Left<>Right inputs. + // However it means that with the current code, a BeginMenu() from outside another menu or a menu-bar won't be closable with the Left direction. + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (g.NavWindow && g.NavWindow->ParentWindow == window && g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet() && window->DC.LayoutType == ImGuiLayoutType_Vertical) + { + ClosePopupToLevel(g.OpenPopupStack.Size - 1); + NavMoveRequestCancel(); + } + + EndPopup(); +} + +// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags) +{ + ImGuiContext& g = *GImGui; + + int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); + BeginTooltipEx(0, true); + + const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text; + if (text_end > text) + { + TextUnformatted(text, text_end); + Separator(); + } + + ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2); + ColorButton("##preview", ImVec4(col[0], col[1], col[2], col[3]), (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz); + SameLine(); + if (flags & ImGuiColorEditFlags_NoAlpha) + Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]); + else + Text("#%02X%02X%02X%02X\nR:%d, G:%d, B:%d, A:%d\n(%.3f, %.3f, %.3f, %.3f)", cr, cg, cb, ca, cr, cg, cb, ca, col[0], col[1], col[2], col[3]); + EndTooltip(); +} + +static inline ImU32 ImAlphaBlendColor(ImU32 col_a, ImU32 col_b) +{ + float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; + int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t); + int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t); + int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t); + return IM_COL32(r, g, b, 0xFF); +} + +// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. +// I spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding alltogether. +void ImGui::RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, int rounding_corners_flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) + { + ImU32 col_bg1 = GetColorU32(ImAlphaBlendColor(IM_COL32(204,204,204,255), col)); + ImU32 col_bg2 = GetColorU32(ImAlphaBlendColor(IM_COL32(128,128,128,255), col)); + window->DrawList->AddRectFilled(p_min, p_max, col_bg1, rounding, rounding_corners_flags); + + int yi = 0; + for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) + { + float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); + if (y2 <= y1) + continue; + for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) + { + float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); + if (x2 <= x1) + continue; + int rounding_corners_flags_cell = 0; + if (y1 <= p_min.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopRight; } + if (y2 >= p_max.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotRight; } + rounding_corners_flags_cell &= rounding_corners_flags; + window->DrawList->AddRectFilled(ImVec2(x1,y1), ImVec2(x2,y2), col_bg2, rounding_corners_flags_cell ? rounding : 0.0f, rounding_corners_flags_cell); + } + } + } + else + { + window->DrawList->AddRectFilled(p_min, p_max, col, rounding, rounding_corners_flags); + } +} + +void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) +{ + ImGuiContext& g = *GImGui; + if ((flags & ImGuiColorEditFlags__InputsMask) == 0) + flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__InputsMask; + if ((flags & ImGuiColorEditFlags__DataTypeMask) == 0) + flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DataTypeMask; + if ((flags & ImGuiColorEditFlags__PickerMask) == 0) + flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__PickerMask; + IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__InputsMask))); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__DataTypeMask))); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__PickerMask))); // Check only 1 option is selected + g.ColorEditOptions = flags; +} + +// A little colored square. Return true when clicked. +// FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip. +// 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip. +bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, ImVec2 size) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(desc_id); + float default_size = GetFrameHeight(); + if (size.x == 0.0f) + size.x = default_size; + if (size.y == 0.0f) + size.y = default_size; + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + + if (flags & ImGuiColorEditFlags_NoAlpha) + flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf); + + ImVec4 col_without_alpha(col.x, col.y, col.z, 1.0f); + float grid_step = ImMin(size.x, size.y) / 2.99f; + float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f); + ImRect bb_inner = bb; + float off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts. + bb_inner.Expand(off); + if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col.w < 1.0f) + { + float mid_x = (float)(int)((bb_inner.Min.x + bb_inner.Max.x) * 0.5f + 0.5f); + RenderColorRectWithAlphaCheckerboard(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawCornerFlags_TopRight| ImDrawCornerFlags_BotRight); + window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_without_alpha), rounding, ImDrawCornerFlags_TopLeft|ImDrawCornerFlags_BotLeft); + } + else + { + // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha + ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col : col_without_alpha; + if (col_source.w < 1.0f) + RenderColorRectWithAlphaCheckerboard(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); + else + window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding, ImDrawCornerFlags_All); + } + RenderNavHighlight(bb, id); + if (g.Style.FrameBorderSize > 0.0f) + RenderFrameBorder(bb.Min, bb.Max, rounding); + else + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border + + // Drag and Drop Source + if (g.ActiveId == id && BeginDragDropSource()) // NB: The ActiveId test is merely an optional micro-optimization + { + if (flags & ImGuiColorEditFlags_NoAlpha) + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F, &col, sizeof(float) * 3, ImGuiCond_Once); + else + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, &col, sizeof(float) * 4, ImGuiCond_Once); + ColorButton(desc_id, col, flags); + SameLine(); + TextUnformatted("Color"); + EndDragDropSource(); + hovered = false; + } + + // Tooltip + if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered) + ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); + + return pressed; +} + +bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags) +{ + return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); +} + +void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) +{ + bool allow_opt_inputs = !(flags & ImGuiColorEditFlags__InputsMask); + bool allow_opt_datatype = !(flags & ImGuiColorEditFlags__DataTypeMask); + if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) + return; + ImGuiContext& g = *GImGui; + ImGuiColorEditFlags opts = g.ColorEditOptions; + if (allow_opt_inputs) + { + if (RadioButton("RGB", (opts & ImGuiColorEditFlags_RGB) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_RGB; + if (RadioButton("HSV", (opts & ImGuiColorEditFlags_HSV) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HSV; + if (RadioButton("HEX", (opts & ImGuiColorEditFlags_HEX) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__InputsMask) | ImGuiColorEditFlags_HEX; + } + if (allow_opt_datatype) + { + if (allow_opt_inputs) Separator(); + if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Uint8; + if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) ? 1 : 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Float; + } + + if (allow_opt_inputs || allow_opt_datatype) + Separator(); + if (Button("Copy as..", ImVec2(-1,0))) + OpenPopup("Copy"); + if (BeginPopup("Copy")) + { + int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); + char buf[64]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%.3ff, %.3ff, %.3ff, %.3ff)", col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + if (Selectable(buf)) + SetClipboardText(buf); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + if (flags & ImGuiColorEditFlags_NoAlpha) + ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X", cr, cg, cb); + else + ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X%02X", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + EndPopup(); + } + + g.ColorEditOptions = opts; + EndPopup(); +} + +static void ColorPickerOptionsPopup(ImGuiColorEditFlags flags, const float* ref_col) +{ + bool allow_opt_picker = !(flags & ImGuiColorEditFlags__PickerMask); + bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar); + if ((!allow_opt_picker && !allow_opt_alpha_bar) || !ImGui::BeginPopup("context")) + return; + ImGuiContext& g = *GImGui; + if (allow_opt_picker) + { + ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function + ImGui::PushItemWidth(picker_size.x); + for (int picker_type = 0; picker_type < 2; picker_type++) + { + // Draw small/thumbnail version of each picker type (over an invisible button for selection) + if (picker_type > 0) ImGui::Separator(); + ImGui::PushID(picker_type); + ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs|ImGuiColorEditFlags_NoOptions|ImGuiColorEditFlags_NoLabel|ImGuiColorEditFlags_NoSidePreview|(flags & ImGuiColorEditFlags_NoAlpha); + if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar; + if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel; + ImVec2 backup_pos = ImGui::GetCursorScreenPos(); + if (ImGui::Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup + g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags__PickerMask) | (picker_flags & ImGuiColorEditFlags__PickerMask); + ImGui::SetCursorScreenPos(backup_pos); + ImVec4 dummy_ref_col; + memcpy(&dummy_ref_col.x, ref_col, sizeof(float) * (picker_flags & ImGuiColorEditFlags_NoAlpha ? 3 : 4)); + ImGui::ColorPicker4("##dummypicker", &dummy_ref_col.x, picker_flags); + ImGui::PopID(); + } + ImGui::PopItemWidth(); + } + if (allow_opt_alpha_bar) + { + if (allow_opt_picker) ImGui::Separator(); + ImGui::CheckboxFlags("Alpha Bar", (unsigned int*)&g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); + } + ImGui::EndPopup(); +} + +// Edit colors components (each component in 0.0f..1.0f range). +// See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +// With typical options: Left-click on colored square to open color picker. Right-click to open option menu. CTRL-Click over input fields to edit them and TAB to go to next item. +bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiStyle& style = g.Style; + const float square_sz = GetFrameHeight(); + const float w_extra = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); + const float w_items_all = CalcItemWidth() - w_extra; + const char* label_display_end = FindRenderedTextEnd(label); + + const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; + const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; + const int components = alpha ? 4 : 3; + const ImGuiColorEditFlags flags_untouched = flags; + + BeginGroup(); + PushID(label); + + // If we're not showing any slider there's no point in doing any HSV conversions + if (flags & ImGuiColorEditFlags_NoInputs) + flags = (flags & (~ImGuiColorEditFlags__InputsMask)) | ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_NoOptions; + + // Context menu: display and modify options (before defaults are applied) + if (!(flags & ImGuiColorEditFlags_NoOptions)) + ColorEditOptionsPopup(col, flags); + + // Read stored options + if (!(flags & ImGuiColorEditFlags__InputsMask)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags__InputsMask); + if (!(flags & ImGuiColorEditFlags__DataTypeMask)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DataTypeMask); + if (!(flags & ImGuiColorEditFlags__PickerMask)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags__PickerMask); + flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask)); + + // Convert to the formats we need + float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f }; + if (flags & ImGuiColorEditFlags_HSV) + ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); + int i[4] = { IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3]) }; + + bool value_changed = false; + bool value_changed_as_float = false; + + if ((flags & (ImGuiColorEditFlags_RGB | ImGuiColorEditFlags_HSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) + { + // RGB/HSV 0..255 Sliders + const float w_item_one = ImMax(1.0f, (float)(int)((w_items_all - (style.ItemInnerSpacing.x) * (components-1)) / (float)components)); + const float w_item_last = ImMax(1.0f, (float)(int)(w_items_all - (w_item_one + style.ItemInnerSpacing.x) * (components-1))); + + const bool hide_prefix = (w_item_one <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); + const char* ids[4] = { "##X", "##Y", "##Z", "##W" }; + const char* fmt_table_int[3][4] = + { + { "%3.0f", "%3.0f", "%3.0f", "%3.0f" }, // Short display + { "R:%3.0f", "G:%3.0f", "B:%3.0f", "A:%3.0f" }, // Long display for RGBA + { "H:%3.0f", "S:%3.0f", "V:%3.0f", "A:%3.0f" } // Long display for HSVA + }; + const char* fmt_table_float[3][4] = + { + { "%0.3f", "%0.3f", "%0.3f", "%0.3f" }, // Short display + { "R:%0.3f", "G:%0.3f", "B:%0.3f", "A:%0.3f" }, // Long display for RGBA + { "H:%0.3f", "S:%0.3f", "V:%0.3f", "A:%0.3f" } // Long display for HSVA + }; + const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_HSV) ? 2 : 1; + + PushItemWidth(w_item_one); + for (int n = 0; n < components; n++) + { + if (n > 0) + SameLine(0, style.ItemInnerSpacing.x); + if (n + 1 == components) + PushItemWidth(w_item_last); + if (flags & ImGuiColorEditFlags_Float) + value_changed = value_changed_as_float = value_changed | DragFloat(ids[n], &f[n], 1.0f/255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); + else + value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + } + PopItemWidth(); + PopItemWidth(); + } + else if ((flags & ImGuiColorEditFlags_HEX) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) + { + // RGB Hexadecimal Input + char buf[64]; + if (alpha) + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255), ImClamp(i[3],0,255)); + else + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255)); + PushItemWidth(w_items_all); + if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase)) + { + value_changed = true; + char* p = buf; + while (*p == '#' || ImCharIsSpace(*p)) + p++; + i[0] = i[1] = i[2] = i[3] = 0; + if (alpha) + sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned) + else + sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]); + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + PopItemWidth(); + } + + ImGuiWindow* picker_active_window = NULL; + if (!(flags & ImGuiColorEditFlags_NoSmallPreview)) + { + if (!(flags & ImGuiColorEditFlags_NoInputs)) + SameLine(0, style.ItemInnerSpacing.x); + + const ImVec4 col_v4(col[0], col[1], col[2], alpha ? col[3] : 1.0f); + if (ColorButton("##ColorButton", col_v4, flags)) + { + if (!(flags & ImGuiColorEditFlags_NoPicker)) + { + // Store current color and open a picker + g.ColorPickerRef = col_v4; + OpenPopup("picker"); + SetNextWindowPos(window->DC.LastItemRect.GetBL() + ImVec2(-1,style.ItemSpacing.y)); + } + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + + if (BeginPopup("picker")) + { + picker_active_window = g.CurrentWindow; + if (label != label_display_end) + { + TextUnformatted(label, label_display_end); + Separator(); + } + ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; + ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags__InputsMask | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; + PushItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? + value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); + PopItemWidth(); + EndPopup(); + } + } + + if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) + { + SameLine(0, style.ItemInnerSpacing.x); + TextUnformatted(label, label_display_end); + } + + // Convert back + if (picker_active_window == NULL) + { + if (!value_changed_as_float) + for (int n = 0; n < 4; n++) + f[n] = i[n] / 255.0f; + if (flags & ImGuiColorEditFlags_HSV) + ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); + if (value_changed) + { + col[0] = f[0]; + col[1] = f[1]; + col[2] = f[2]; + if (alpha) + col[3] = f[3]; + } + } + + PopID(); + EndGroup(); + + // Drag and Drop Target + if ((window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) && BeginDragDropTarget()) // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. + { + if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) + { + memcpy((float*)col, payload->Data, sizeof(float) * 3); + value_changed = true; + } + if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) + { + memcpy((float*)col, payload->Data, sizeof(float) * components); + value_changed = true; + } + EndDragDropTarget(); + } + + // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4(). + if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window) + window->DC.LastItemId = g.ActiveId; + + return value_changed; +} + +bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags) +{ + float col4[4] = { col[0], col[1], col[2], 1.0f }; + if (!ColorPicker4(label, col4, flags | ImGuiColorEditFlags_NoAlpha)) + return false; + col[0] = col4[0]; col[1] = col4[1]; col[2] = col4[2]; + return true; +} + +// 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. +static void RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col) +{ + switch (direction) + { + case ImGuiDir_Left: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Right: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_Up: draw_list->AddTriangleFilled(ImVec2(pos.x + half_sz.x, pos.y + half_sz.y), ImVec2(pos.x - half_sz.x, pos.y + half_sz.y), pos, col); return; + case ImGuiDir_Down: draw_list->AddTriangleFilled(ImVec2(pos.x - half_sz.x, pos.y - half_sz.y), ImVec2(pos.x + half_sz.x, pos.y - half_sz.y), pos, col); return; + case ImGuiDir_None: case ImGuiDir_Count_: break; // Fix warnings + } +} + +static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w) +{ + RenderArrow(draw_list, ImVec2(pos.x + half_sz.x + 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Right, IM_COL32_BLACK); + RenderArrow(draw_list, ImVec2(pos.x + half_sz.x, pos.y), half_sz, ImGuiDir_Right, IM_COL32_WHITE); + RenderArrow(draw_list, ImVec2(pos.x + bar_w - half_sz.x - 1, pos.y), ImVec2(half_sz.x + 2, half_sz.y + 1), ImGuiDir_Left, IM_COL32_BLACK); + RenderArrow(draw_list, ImVec2(pos.x + bar_w - half_sz.x, pos.y), half_sz, ImGuiDir_Left, IM_COL32_WHITE); +} + +// ColorPicker +// Note: only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. +// FIXME: we adjust the big color square height based on item width, which may cause a flickering feedback loop (if automatic height makes a vertical scrollbar appears, affecting automatic width..) +bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags, const float* ref_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + ImDrawList* draw_list = window->DrawList; + + ImGuiStyle& style = g.Style; + ImGuiIO& io = g.IO; + + PushID(label); + BeginGroup(); + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + flags |= ImGuiColorEditFlags_NoSmallPreview; + + // Context menu: display and store options. + if (!(flags & ImGuiColorEditFlags_NoOptions)) + ColorPickerOptionsPopup(flags, col); + + // Read stored options + if (!(flags & ImGuiColorEditFlags__PickerMask)) + flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__PickerMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__PickerMask; + IM_ASSERT(ImIsPowerOfTwo((int)(flags & ImGuiColorEditFlags__PickerMask))); // Check that only 1 is selected + if (!(flags & ImGuiColorEditFlags_NoOptions)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar); + + // Setup + int components = (flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4; + bool alpha_bar = (flags & ImGuiColorEditFlags_AlphaBar) && !(flags & ImGuiColorEditFlags_NoAlpha); + ImVec2 picker_pos = window->DC.CursorPos; + float square_sz = GetFrameHeight(); + float bars_width = square_sz; // Arbitrary smallish width of Hue/Alpha picking bars + float sv_picker_size = ImMax(bars_width * 1, CalcItemWidth() - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box + float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; + float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; + float bars_triangles_half_sz = (float)(int)(bars_width * 0.20f); + + float backup_initial_col[4]; + memcpy(backup_initial_col, col, components * sizeof(float)); + + float wheel_thickness = sv_picker_size * 0.08f; + float wheel_r_outer = sv_picker_size * 0.50f; + float wheel_r_inner = wheel_r_outer - wheel_thickness; + ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size*0.5f); + + // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic. + float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f); + ImVec2 triangle_pa = ImVec2(triangle_r, 0.0f); // Hue point. + ImVec2 triangle_pb = ImVec2(triangle_r * -0.5f, triangle_r * -0.866025f); // Black point. + ImVec2 triangle_pc = ImVec2(triangle_r * -0.5f, triangle_r * +0.866025f); // White point. + + float H,S,V; + ColorConvertRGBtoHSV(col[0], col[1], col[2], H, S, V); + + bool value_changed = false, value_changed_h = false, value_changed_sv = false; + + PushItemFlag(ImGuiItemFlags_NoNav, true); + if (flags & ImGuiColorEditFlags_PickerHueWheel) + { + // Hue wheel + SV triangle logic + InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); + if (IsItemActive()) + { + ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; + ImVec2 current_off = g.IO.MousePos - wheel_center; + float initial_dist2 = ImLengthSqr(initial_off); + if (initial_dist2 >= (wheel_r_inner-1)*(wheel_r_inner-1) && initial_dist2 <= (wheel_r_outer+1)*(wheel_r_outer+1)) + { + // Interactive with Hue wheel + H = atan2f(current_off.y, current_off.x) / IM_PI*0.5f; + if (H < 0.0f) + H += 1.0f; + value_changed = value_changed_h = true; + } + float cos_hue_angle = cosf(-H * 2.0f * IM_PI); + float sin_hue_angle = sinf(-H * 2.0f * IM_PI); + if (ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, ImRotate(initial_off, cos_hue_angle, sin_hue_angle))) + { + // Interacting with SV triangle + ImVec2 current_off_unrotated = ImRotate(current_off, cos_hue_angle, sin_hue_angle); + if (!ImTriangleContainsPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated)) + current_off_unrotated = ImTriangleClosestPoint(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated); + float uu, vv, ww; + ImTriangleBarycentricCoords(triangle_pa, triangle_pb, triangle_pc, current_off_unrotated, uu, vv, ww); + V = ImClamp(1.0f - vv, 0.0001f, 1.0f); + S = ImClamp(uu / V, 0.0001f, 1.0f); + value_changed = value_changed_sv = true; + } + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + } + else if (flags & ImGuiColorEditFlags_PickerHueBar) + { + // SV rectangle logic + InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); + if (IsItemActive()) + { + S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size-1)); + V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + value_changed = value_changed_sv = true; + } + if (!(flags & ImGuiColorEditFlags_NoOptions)) + OpenPopupOnItemClick("context"); + + // Hue bar logic + SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); + InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); + if (IsItemActive()) + { + H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + value_changed = value_changed_h = true; + } + } + + // Alpha bar logic + if (alpha_bar) + { + SetCursorScreenPos(ImVec2(bar1_pos_x, picker_pos.y)); + InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size)); + if (IsItemActive()) + { + col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + value_changed = true; + } + } + PopItemFlag(); // ImGuiItemFlags_NoNav + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + { + SameLine(0, style.ItemInnerSpacing.x); + BeginGroup(); + } + + if (!(flags & ImGuiColorEditFlags_NoLabel)) + { + const char* label_display_end = FindRenderedTextEnd(label); + if (label != label_display_end) + { + if ((flags & ImGuiColorEditFlags_NoSidePreview)) + SameLine(0, style.ItemInnerSpacing.x); + TextUnformatted(label, label_display_end); + } + } + + if (!(flags & ImGuiColorEditFlags_NoSidePreview)) + { + PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true); + ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); + if ((flags & ImGuiColorEditFlags_NoLabel)) + Text("Current"); + ColorButton("##current", col_v4, (flags & (ImGuiColorEditFlags_HDR|ImGuiColorEditFlags_AlphaPreview|ImGuiColorEditFlags_AlphaPreviewHalf|ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2)); + if (ref_col != NULL) + { + Text("Original"); + ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]); + if (ColorButton("##original", ref_col_v4, (flags & (ImGuiColorEditFlags_HDR|ImGuiColorEditFlags_AlphaPreview|ImGuiColorEditFlags_AlphaPreviewHalf|ImGuiColorEditFlags_NoTooltip)), ImVec2(square_sz * 3, square_sz * 2))) + { + memcpy(col, ref_col, components * sizeof(float)); + value_changed = true; + } + } + PopItemFlag(); + EndGroup(); + } + + // Convert back color to RGB + if (value_changed_h || value_changed_sv) + ColorConvertHSVtoRGB(H >= 1.0f ? H - 10 * 1e-6f : H, S > 0.0f ? S : 10*1e-6f, V > 0.0f ? V : 1e-6f, col[0], col[1], col[2]); + + // R,G,B and H,S,V slider color editor + if ((flags & ImGuiColorEditFlags_NoInputs) == 0) + { + PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; + ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; + if (flags & ImGuiColorEditFlags_RGB || (flags & ImGuiColorEditFlags__InputsMask) == 0) + value_changed |= ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_RGB); + if (flags & ImGuiColorEditFlags_HSV || (flags & ImGuiColorEditFlags__InputsMask) == 0) + value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_HSV); + if (flags & ImGuiColorEditFlags_HEX || (flags & ImGuiColorEditFlags__InputsMask) == 0) + value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_HEX); + PopItemWidth(); + } + + // Try to cancel hue wrap (after ColorEdit), if any + if (value_changed) + { + float new_H, new_S, new_V; + ColorConvertRGBtoHSV(col[0], col[1], col[2], new_H, new_S, new_V); + if (new_H <= 0 && H > 0) + { + if (new_V <= 0 && V != new_V) + ColorConvertHSVtoRGB(H, S, new_V <= 0 ? V * 0.5f : new_V, col[0], col[1], col[2]); + else if (new_S <= 0) + ColorConvertHSVtoRGB(H, new_S <= 0 ? S * 0.5f : new_S, new_V, col[0], col[1], col[2]); + } + } + + ImVec4 hue_color_f(1, 1, 1, 1); ColorConvertHSVtoRGB(H, 1, 1, hue_color_f.x, hue_color_f.y, hue_color_f.z); + ImU32 hue_color32 = ColorConvertFloat4ToU32(hue_color_f); + ImU32 col32_no_alpha = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 1.0f)); + + const ImU32 hue_colors[6+1] = { IM_COL32(255,0,0,255), IM_COL32(255,255,0,255), IM_COL32(0,255,0,255), IM_COL32(0,255,255,255), IM_COL32(0,0,255,255), IM_COL32(255,0,255,255), IM_COL32(255,0,0,255) }; + ImVec2 sv_cursor_pos; + + if (flags & ImGuiColorEditFlags_PickerHueWheel) + { + // Render Hue Wheel + const float aeps = 1.5f / wheel_r_outer; // Half a pixel arc length in radians (2pi cancels out). + const int segment_per_arc = ImMax(4, (int)wheel_r_outer / 12); + for (int n = 0; n < 6; n++) + { + const float a0 = (n) /6.0f * 2.0f * IM_PI - aeps; + const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps; + const int vert_start_idx = draw_list->VtxBuffer.Size; + draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc); + draw_list->PathStroke(IM_COL32_WHITE, false, wheel_thickness); + const int vert_end_idx = draw_list->VtxBuffer.Size; + + // Paint colors over existing vertices + ImVec2 gradient_p0(wheel_center.x + cosf(a0) * wheel_r_inner, wheel_center.y + sinf(a0) * wheel_r_inner); + ImVec2 gradient_p1(wheel_center.x + cosf(a1) * wheel_r_inner, wheel_center.y + sinf(a1) * wheel_r_inner); + ShadeVertsLinearColorGradientKeepAlpha(draw_list->VtxBuffer.Data + vert_start_idx, draw_list->VtxBuffer.Data + vert_end_idx, gradient_p0, gradient_p1, hue_colors[n], hue_colors[n+1]); + } + + // Render Cursor + preview on Hue Wheel + float cos_hue_angle = cosf(H * 2.0f * IM_PI); + float sin_hue_angle = sinf(H * 2.0f * IM_PI); + ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner+wheel_r_outer)*0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner+wheel_r_outer)*0.5f); + float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f; + int hue_cursor_segments = ImClamp((int)(hue_cursor_rad / 1.4f), 9, 32); + draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad+1, IM_COL32(128,128,128,255), hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, IM_COL32_WHITE, hue_cursor_segments); + + // Render SV triangle (rotated according to hue) + ImVec2 tra = wheel_center + ImRotate(triangle_pa, cos_hue_angle, sin_hue_angle); + ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle); + ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle); + ImVec2 uv_white = GetFontTexUvWhitePixel(); + draw_list->PrimReserve(6, 6); + draw_list->PrimVtx(tra, uv_white, hue_color32); + draw_list->PrimVtx(trb, uv_white, hue_color32); + draw_list->PrimVtx(trc, uv_white, IM_COL32_WHITE); + draw_list->PrimVtx(tra, uv_white, IM_COL32_BLACK_TRANS); + draw_list->PrimVtx(trb, uv_white, IM_COL32_BLACK); + draw_list->PrimVtx(trc, uv_white, IM_COL32_BLACK_TRANS); + draw_list->AddTriangle(tra, trb, trc, IM_COL32(128,128,128,255), 1.5f); + sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V)); + } + else if (flags & ImGuiColorEditFlags_PickerHueBar) + { + // Render SV Square + draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), IM_COL32_WHITE, hue_color32, hue_color32, IM_COL32_WHITE); + draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, IM_COL32_BLACK, IM_COL32_BLACK); + RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size,sv_picker_size), 0.0f); + sv_cursor_pos.x = ImClamp((float)(int)(picker_pos.x + ImSaturate(S) * sv_picker_size + 0.5f), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much + sv_cursor_pos.y = ImClamp((float)(int)(picker_pos.y + ImSaturate(1 - V) * sv_picker_size + 0.5f), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); + + // Render Hue Bar + for (int i = 0; i < 6; ++i) + draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), hue_colors[i], hue_colors[i], hue_colors[i + 1], hue_colors[i + 1]); + float bar0_line_y = (float)(int)(picker_pos.y + H * sv_picker_size + 0.5f); + RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f); + RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f); + } + + // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) + float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f; + draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, col32_no_alpha, 12); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad+1, IM_COL32(128,128,128,255), 12); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, IM_COL32_WHITE, 12); + + // Render alpha bar + if (alpha_bar) + { + float alpha = ImSaturate(col[3]); + ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size); + RenderColorRectWithAlphaCheckerboard(bar1_bb.Min, bar1_bb.Max, IM_COL32(0,0,0,0), bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); + draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, col32_no_alpha, col32_no_alpha, col32_no_alpha & ~IM_COL32_A_MASK, col32_no_alpha & ~IM_COL32_A_MASK); + float bar1_line_y = (float)(int)(picker_pos.y + (1.0f - alpha) * sv_picker_size + 0.5f); + RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f); + RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f); + } + + EndGroup(); + PopID(); + + return value_changed && memcmp(backup_initial_col, col, components * sizeof(float)); +} + +// Horizontal separating line. +void ImGui::Separator() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + ImGuiWindowFlags flags = 0; + if ((flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical)) == 0) + flags |= (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal; + IM_ASSERT(ImIsPowerOfTwo((int)(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical)))); // Check that only 1 option is selected + if (flags & ImGuiSeparatorFlags_Vertical) + { + VerticalSeparator(); + return; + } + + // Horizontal Separator + if (window->DC.ColumnsSet) + PopClipRect(); + + float x1 = window->Pos.x; + float x2 = window->Pos.x + window->Size.x; + if (!window->DC.GroupStack.empty()) + x1 += window->DC.IndentX; + + const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y+1.0f)); + ItemSize(ImVec2(0.0f, 0.0f)); // NB: we don't provide our width so that it doesn't get feed back into AutoFit, we don't provide height to not alter layout. + if (!ItemAdd(bb, 0)) + { + if (window->DC.ColumnsSet) + PushColumnClipRect(); + return; + } + + window->DrawList->AddLine(bb.Min, ImVec2(bb.Max.x,bb.Min.y), GetColorU32(ImGuiCol_Separator)); + + if (g.LogEnabled) + LogRenderedText(NULL, IM_NEWLINE "--------------------------------"); + + if (window->DC.ColumnsSet) + { + PushColumnClipRect(); + window->DC.ColumnsSet->CellMinY = window->DC.CursorPos.y; + } +} + +void ImGui::VerticalSeparator() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + ImGuiContext& g = *GImGui; + + float y1 = window->DC.CursorPos.y; + float y2 = window->DC.CursorPos.y + window->DC.CurrentLineHeight; + const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + 1.0f, y2)); + ItemSize(ImVec2(bb.GetWidth(), 0.0f)); + if (!ItemAdd(bb, 0)) + return; + + window->DrawList->AddLine(ImVec2(bb.Min.x, bb.Min.y), ImVec2(bb.Min.x, bb.Max.y), GetColorU32(ImGuiCol_Separator)); + if (g.LogEnabled) + LogText(" |"); +} + +bool 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; + + 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(); + + 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)); + } + + // 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)); +} + +void ImGui::Dummy(const ImVec2& size) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); + ItemSize(bb); + ItemAdd(bb, 0); +} + +bool ImGui::IsRectVisible(const ImVec2& 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) +{ + 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(); + + 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; +} + +void ImGui::EndGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + IM_ASSERT(!window->DC.GroupStack.empty()); // Mismatched BeginGroup()/EndGroup() calls + + ImGuiGroupData& group_data = window->DC.GroupStack.back(); + + ImRect group_bb(group_data.BackupCursorPos, window->DC.CursorMaxPos); + group_bb.Max = ImMax(group_bb.Min, group_bb.Max); + + window->DC.CursorPos = group_data.BackupCursorPos; + window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, window->DC.CursorMaxPos); + window->DC.CurrentLineHeight = group_data.BackupCurrentLineHeight; + window->DC.CurrentLineTextBaseOffset = group_data.BackupCurrentLineTextBaseOffset; + window->DC.IndentX = group_data.BackupIndentX; + window->DC.GroupOffsetX = group_data.BackupGroupOffsetX; + window->DC.LogLinePosY = window->DC.CursorPos.y - 9999.0f; + + if (group_data.AdvanceCursor) + { + window->DC.CurrentLineTextBaseOffset = ImMax(window->DC.PrevLineTextBaseOffset, group_data.BackupCurrentLineTextBaseOffset); // FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now. + ItemSize(group_bb.GetSize(), group_data.BackupCurrentLineTextBaseOffset); + ItemAdd(group_bb, 0); + } + + // If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive() will be functional on the entire group. + // It would be be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but if you search for LastItemId you'll notice it is only used in that context. + const bool active_id_within_group = (!group_data.BackupActiveIdIsAlive && g.ActiveIdIsAlive && g.ActiveId && g.ActiveIdWindow->RootWindow == window->RootWindow); + if (active_id_within_group) + window->DC.LastItemId = g.ActiveId; + window->DC.LastItemRect = group_bb; + + window->DC.GroupStack.pop_back(); + + //window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] +} + +// Gets back to previous line and continue with horizontal layout +// pos_x == 0 : follow right after previous item +// pos_x != 0 : align to specified x position (relative to window/group left) +// spacing_w < 0 : use default spacing if pos_x == 0, no spacing if pos_x != 0 +// spacing_w >= 0 : enforce spacing amount +void ImGui::SameLine(float pos_x, float spacing_w) +{ + 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; + + 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 +} + +int ImGui::GetColumnIndex() +{ + 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; +} + +static float OffsetNormToPixels(const ImGuiColumnsSet* columns, float offset_norm) +{ + return offset_norm * (columns->MaxX - columns->MinX); +} + +static float PixelsToOffsetNorm(const ImGuiColumnsSet* columns, float offset) +{ + return offset / (columns->MaxX - columns->MinX); +} + +static inline float GetColumnsRectHalfWidth() { return 4.0f; } + +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)); + + 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; +} + +float ImGui::GetColumnOffset(int column_index) +{ + 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 (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; +} + +static float GetColumnWidthEx(ImGuiColumnsSet* columns, int column_index, bool before_resize = false) +{ + 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 ImGui::GetColumnWidth(int column_index) +{ + 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); +} + +void ImGui::SetColumnOffset(int column_index, float offset) +{ + 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); + + 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 (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); + + 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); +} + +void ImGui::EndColumns() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + ImGuiColumnsSet* columns = window->DC.ColumnsSet; + IM_ASSERT(columns != NULL); + + PopItemWidth(); + PopClipRect(); + window->DrawList->ChannelsMerge(); + + columns->CellMaxY = ImMax(columns->CellMaxY, window->DC.CursorPos.y); + window->DC.CursorPos.y = columns->CellMaxY; + if (!(columns->Flags & ImGuiColumnsFlags_GrowParentContentsSize)) + window->DC.CursorMaxPos.x = ImMax(columns->StartMaxPosX, columns->MaxX); // Restore cursor max pos, as columns don't grow parent + + // Draw columns borders and handle resize + bool is_being_resized = false; + if (!(columns->Flags & ImGuiColumnsFlags_NoBorder) && !window->SkipItems) + { + const float y1 = columns->StartPosY; + const float y2 = window->DC.CursorPos.y; + int dragging_column = -1; + for (int n = 1; n < columns->Count; n++) + { + float x = window->Pos.x + GetColumnOffset(n); + const ImGuiID column_id = columns->ID + ImGuiID(n); + const float column_hw = GetColumnsRectHalfWidth(); // Half-width for interaction + const ImRect column_rect(ImVec2(x - column_hw, y1), ImVec2(x + column_hw, y2)); + KeepAliveID(column_id); + if (IsClippedEx(column_rect, column_id, false)) + continue; + + bool hovered = false, held = false; + if (!(columns->Flags & ImGuiColumnsFlags_NoResize)) + { + ButtonBehavior(column_rect, column_id, &hovered, &held); + if (hovered || held) + g.MouseCursor = ImGuiMouseCursor_ResizeEW; + if (held && !(columns->Columns[n].Flags & ImGuiColumnsFlags_NoResize)) + dragging_column = n; + } + + // Draw column (we clip the Y boundaries CPU side because very long triangles are mishandled by some GPU drivers.) + const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); + const float xi = (float)(int)x; + window->DrawList->AddLine(ImVec2(xi, ImMax(y1 + 1.0f, window->ClipRect.Min.y)), ImVec2(xi, ImMin(y2, window->ClipRect.Max.y)), col); + } + + // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame. + if (dragging_column != -1) + { + if (!columns->IsBeingResized) + for (int n = 0; n < columns->Count + 1; n++) + columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm; + columns->IsBeingResized = is_being_resized = true; + float x = GetDraggedColumnOffset(columns, dragging_column); + SetColumnOffset(dragging_column, x); + } + } + columns->IsBeingResized = is_being_resized; + + window->DC.ColumnsSet = NULL; + window->DC.ColumnsOffsetX = 0.0f; + window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.IndentX + window->DC.ColumnsOffsetX); +} + +// [2017/12: This is currently the only public API, while we are working on making BeginColumns/EndColumns user-facing] +void ImGui::Columns(int columns_count, const char* id, bool border) +{ + ImGuiWindow* window = GetCurrentWindow(); + IM_ASSERT(columns_count >= 1); + if (window->DC.ColumnsSet != NULL && window->DC.ColumnsSet->Count != columns_count) + EndColumns(); + + ImGuiColumnsFlags flags = (border ? 0 : ImGuiColumnsFlags_NoBorder); + //flags |= ImGuiColumnsFlags_NoPreserveWidths; // NB: Legacy behavior + if (columns_count != 1) + BeginColumns(id, columns_count, flags); +} + +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; +} + +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; +} + +void ImGui::TreePush(const char* str_id) +{ + ImGuiWindow* window = GetCurrentWindow(); + Indent(); + window->DC.TreeDepth++; + PushID(str_id ? str_id : "#TreePush"); +} + +void ImGui::TreePush(const void* ptr_id) +{ + 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); +} + +void ImGui::TreePop() +{ + 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; + + PopID(); +} + +void ImGui::Value(const char* prefix, bool b) +{ + Text("%s: %s", prefix, (b ? "true" : "false")); +} + +void ImGui::Value(const char* prefix, int v) +{ + Text("%s: %d", prefix, v); +} + +void ImGui::Value(const char* prefix, unsigned int v) +{ + Text("%s: %d", prefix, v); +} + +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); + } +} + +//----------------------------------------------------------------------------- +// DRAG AND DROP +//----------------------------------------------------------------------------- + +void ImGui::ClearDragDrop() +{ + 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. +// 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; +} + +void ImGui::EndDragDropSource() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.DragDropActive); + + if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + EndTooltip(); + PopStyleColor(); + //PopStyleVar(); + } + + // 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; +} + +// We don't use BeginDragDropTargetCustom() and duplicate its code because: +// 1) we use LastItemRectHoveredRect which handles items that pushes a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. +// 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can. +// Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case) +bool ImGui::BeginDragDropTarget() +{ + 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; + + 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; +} + +bool ImGui::IsDragDropPayloadBeingAccepted() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive && g.DragDropAcceptIdPrev != 0; +} + +const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiPayload& payload = g.DragDropPayload; + IM_ASSERT(g.DragDropActive); // Not called between BeginDragDropTarget() and EndDragDropTarget() ? + IM_ASSERT(payload.DataFrameCount != -1); // Forgot to call EndDragDropTarget() ? + if (type != NULL && !payload.IsDataType(type)) + return NULL; + + // Accept smallest drag target bounding box, this allows us to nest drag targets conveniently without ordering constraints. + // NB: We currently accept NULL id as target. However, overlapping targets requires a unique ID to function! + const bool was_accepted_previously = (g.DragDropAcceptIdPrev == g.DragDropTargetId); + ImRect r = g.DragDropTargetRect; + float r_surface = r.GetWidth() * r.GetHeight(); + if (r_surface < g.DragDropAcceptIdCurrRectSurface) + { + g.DragDropAcceptIdCurr = g.DragDropTargetId; + g.DragDropAcceptIdCurrRectSurface = r_surface; + } + + // Render default drop visuals + payload.Preview = was_accepted_previously; + flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that lives for 1 frame) + if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview) + { + // FIXME-DRAG: Settle on a proper default visuals for drop target. + r.Expand(3.5f); + bool push_clip_rect = !window->ClipRect.Contains(r); + if (push_clip_rect) window->DrawList->PushClipRectFullScreen(); + window->DrawList->AddRect(r.Min, r.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, ~0, 2.0f); + if (push_clip_rect) window->DrawList->PopClipRect(); + } + + g.DragDropAcceptFrameCount = g.FrameCount; + payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting os window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() + if (!payload.Delivery && !(flags & ImGuiDragDropFlags_AcceptBeforeDelivery)) + return NULL; + + return &payload; +} + +// We don't really use/need this now, but added it for the sake of consistency and because we might need it later. +void ImGui::EndDragDropTarget() +{ + ImGuiContext& g = *GImGui; (void)g; + IM_ASSERT(g.DragDropActive); +} + +//----------------------------------------------------------------------------- +// PLATFORM DEPENDENT HELPERS +//----------------------------------------------------------------------------- + +#if defined(_WIN32) && !defined(_WINDOWS_) && (!defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) || !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS)) +#undef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#ifndef __MINGW32__ +#include +#else +#include +#endif +#endif + +// 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(); +} + +#else + +// Local ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers +static const char* GetClipboardTextFn_DefaultImpl(void*) +{ + 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) +{ + 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 + +// 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 + +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); + } +} + +#else + +static void ImeSetInputScreenPosFn_DefaultImpl(int, int) {} + +#endif + +//----------------------------------------------------------------------------- +// 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(); +} + +//----------------------------------------------------------------------------- + +// Include imgui_user.inl at the end of imgui.cpp to access private data/functions that aren't exposed. +// Prefer just including imgui_internal.h from your code rather than using this define. If a declaration is missing from imgui_internal.h add it or request it on the github. +#ifdef IMGUI_INCLUDE_IMGUI_USER_INL +#include "imgui_user.inl" +#endif + +//----------------------------------------------------------------------------- diff --git a/attachments/simple_engine/imgui/imgui.h b/attachments/simple_engine/imgui/imgui.h new file mode 100644 index 00000000..bd63a2b0 --- /dev/null +++ b/attachments/simple_engine/imgui/imgui.h @@ -0,0 +1,1787 @@ +// dear imgui, v1.60 WIP +// (headers) + +// See imgui.cpp file for documentation. +// Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp for demo code. +// Read 'Programmer guide' in imgui.cpp for notes on how to setup ImGui in your codebase. +// Get latest version at https://github.com/ocornut/imgui + +#pragma once + +// 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 +#endif +#if !defined(IMGUI_DISABLE_INCLUDE_IMCONFIG_H) || defined(IMGUI_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 + +#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 +#endif + +// Define assertion handler. +#ifndef IM_ASSERT +#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))) +#else +#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++. + +#if defined(__clang__) +#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) + +// Typedefs and Enumerations (declared as int for compatibility and to not pollute the top of this file) +typedef unsigned int ImU32; // 32-bit unsigned integer (typically used to store packed colors) +typedef unsigned int ImGuiID; // unique ID used by widgets (typically hashed from a stack of string) +typedef unsigned short ImWchar; // character for keyboard input/display +typedef void* ImTextureID; // user data to identify a texture (this is whatever to you want it to be! read the FAQ about ImTextureID in imgui.cpp) +typedef int ImGuiCol; // enum: a color identifier for styling // enum ImGuiCol_ +typedef int ImGuiCond; // enum: a condition for Set*() // enum ImGuiCond_ +typedef int ImGuiKey; // enum: a key identifier (ImGui-side enum) // enum ImGuiKey_ +typedef int ImGuiNavInput; // enum: an input identifier for navigation // enum ImGuiNavInput_ +typedef int ImGuiMouseCursor; // enum: a mouse cursor identifier // enum ImGuiMouseCursor_ +typedef int ImGuiStyleVar; // enum: a variable identifier for styling // enum ImGuiStyleVar_ +typedef int ImDrawCornerFlags; // flags: for ImDrawList::AddRect*() etc. // enum ImDrawCornerFlags_ +typedef int ImDrawListFlags; // flags: for ImDrawList // enum ImDrawListFlags_ +typedef int ImFontAtlasFlags; // flags: for ImFontAtlas // enum ImFontAtlasFlags_ +typedef int ImGuiColorEditFlags; // flags: for ColorEdit*(), ColorPicker*() // enum ImGuiColorEditFlags_ +typedef int ImGuiColumnsFlags; // flags: for *Columns*() // enum ImGuiColumnsFlags_ +typedef int ImGuiDragDropFlags; // flags: for *DragDrop*() // enum ImGuiDragDropFlags_ +typedef int ImGuiComboFlags; // flags: for BeginCombo() // enum ImGuiComboFlags_ +typedef int ImGuiFocusedFlags; // flags: for IsWindowFocused() // enum ImGuiFocusedFlags_ +typedef int ImGuiHoveredFlags; // flags: for IsItemHovered() etc. // enum ImGuiHoveredFlags_ +typedef int ImGuiInputTextFlags; // flags: for InputText*() // enum ImGuiInputTextFlags_ +typedef int ImGuiNavFlags; // flags: for io.NavFlags // enum ImGuiNavFlags_ +typedef int ImGuiSelectableFlags; // flags: for Selectable() // enum ImGuiSelectableFlags_ +typedef int ImGuiTreeNodeFlags; // flags: for TreeNode*(),CollapsingHeader()// enum ImGuiTreeNodeFlags_ +typedef int ImGuiWindowFlags; // flags: for Begin*() // enum ImGuiWindowFlags_ +typedef int (*ImGuiTextEditCallback)(ImGuiTextEditCallbackData *data); +typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); +#if defined(_MSC_VER) && !defined(__clang__) +typedef unsigned __int64 ImU64; // 64-bit unsigned integer +#else +typedef unsigned long long ImU64; // 64-bit unsigned integer +#endif + +// Others helpers at bottom of the file: +// class ImVector<> // Lightweight std::vector like class. +// IMGUI_ONCE_UPON_A_FRAME // Execute a block of code once per frame only (convenient for creating UI within deep-nested code that runs multiple times) + +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 +#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 +#endif +}; + +// ImGui end-user API +// In a namespace so that user can add extra functions in a separate file (e.g. Value() helpers for your vector or common types) +namespace ImGui +{ + // Context creation and access, if you want to use multiple context, share context between modules (e.g. DLL). + // All contexts share a same ImFontAtlas by default. If you want different font atlas, you can new() them and overwrite the GetIO().Fonts variable of an ImGui context. + // All those functions are not reliant on the current context. + IMGUI_API ImGuiContext* CreateContext(ImFontAtlas* shared_font_atlas = NULL); + IMGUI_API void DestroyContext(ImGuiContext* ctx = NULL); // NULL = Destroy current context + IMGUI_API ImGuiContext* GetCurrentContext(); + IMGUI_API void SetCurrentContext(ImGuiContext* ctx); + + // Main + IMGUI_API ImGuiIO& GetIO(); + IMGUI_API ImGuiStyle& GetStyle(); + IMGUI_API void NewFrame(); // start a new ImGui frame, you can submit any command from this point until Render()/EndFrame(). + IMGUI_API void Render(); // ends the ImGui frame, finalize the draw data. (Obsolete: optionally call io.RenderDrawListsFn if set. Nowadays, prefer calling your render function yourself.) + IMGUI_API ImDrawData* GetDrawData(); // valid after Render() and until the next call to NewFrame(). this is what you have to render. (Obsolete: this used to be passed to your io.RenderDrawListsFn() function.) + IMGUI_API void EndFrame(); // ends the ImGui frame. automatically called by Render(), so most likely don't need to ever call that yourself directly. If you don't need to render you may call EndFrame() but you'll have wasted CPU already. If you don't need to render, better to not create any imgui windows instead! + + // Demo, Debug, Informations + IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create demo/test window (previously called ShowTestWindow). demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! + IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create metrics window. display ImGui internals: draw commands (with individual draw calls and vertices), window list, basic internal state, etc. + IMGUI_API void ShowStyleEditor(ImGuiStyle* ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) + IMGUI_API bool ShowStyleSelector(const char* label); + IMGUI_API void ShowFontSelector(const char* label); + IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as a end-user (mouse/keyboard controls). + IMGUI_API const char* GetVersion(); + + // Styles + IMGUI_API void StyleColorsDark(ImGuiStyle* dst = NULL); // New, recommended style + IMGUI_API void StyleColorsClassic(ImGuiStyle* dst = NULL); // Classic imgui style (default) + IMGUI_API void StyleColorsLight(ImGuiStyle* dst = NULL); // Best used with borders and a custom, thicker font + + // Window + IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // push window to the stack and start appending to it. see .cpp for details. return false when window is collapsed (so you can early out in your code) but you always need to call End() regardless. 'bool* p_open' creates a widget on the upper-right to close the window (which sets your bool to false). + IMGUI_API void End(); // always call even if Begin() return false (which indicates a collapsed window)! finish appending to current window, pop it off the window stack. + IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0,0), bool border = false, ImGuiWindowFlags flags = 0); // begin a scrolling region. size==0.0f: use remaining window size, size<0.0f: use remaining window size minus abs(size). size>0.0f: fixed size. each axis can use a different mode, e.g. ImVec2(0,400). + IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0,0), bool border = false, ImGuiWindowFlags flags = 0); // " + IMGUI_API void EndChild(); // always call even if BeginChild() return false (which indicates a collapsed or clipping child window) + IMGUI_API ImVec2 GetContentRegionMax(); // current content boundaries (typically window boundaries including scrolling, or current column boundaries), in windows coordinates + IMGUI_API ImVec2 GetContentRegionAvail(); // == GetContentRegionMax() - GetCursorPos() + IMGUI_API float GetContentRegionAvailWidth(); // + IMGUI_API ImVec2 GetWindowContentRegionMin(); // content boundaries min (roughly (0,0)-Scroll), in window coordinates + IMGUI_API ImVec2 GetWindowContentRegionMax(); // content boundaries max (roughly (0,0)+Size-Scroll) where Size can be override with SetNextWindowContentSize(), in window coordinates + IMGUI_API float GetWindowContentRegionWidth(); // + IMGUI_API ImDrawList* GetWindowDrawList(); // get rendering command-list if you want to append your own draw primitives + IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (useful if you want to do your own drawing via the DrawList api) + IMGUI_API ImVec2 GetWindowSize(); // get current window size + IMGUI_API float GetWindowWidth(); + IMGUI_API float GetWindowHeight(); + IMGUI_API bool IsWindowCollapsed(); + IMGUI_API bool IsWindowAppearing(); + IMGUI_API void SetWindowFontScale(float scale); // per-window font scale. Adjust IO.FontGlobalScale if you want to scale all windows + + IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0,0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. + IMGUI_API void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() + IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use -1,-1 on either X/Y axis to preserve the current size. Use callback to apply non-trivial programmatic constraints. + IMGUI_API void SetNextWindowContentSize(const ImVec2& size); // set next window content size (~ enforce the range of scrollbars). not including window decorations (title bar, menu bar, etc.). set an axis to 0.0f to leave it automatic. call before Begin() + IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() + IMGUI_API void SetNextWindowFocus(); // set next window to be focused / front-most. call before Begin() + IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily modify ImGuiCol_WindowBg/ChildBg/PopupBg. + IMGUI_API void SetWindowPos(const ImVec2& pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. + IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0,0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. + IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). + IMGUI_API void SetWindowFocus(); // (not recommended) set current window to be focused / front-most. prefer using SetNextWindowFocus(). + IMGUI_API void SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0); // set named window position. + IMGUI_API void SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0); // set named window size. set axis to 0.0f to force an auto-fit on this axis. + IMGUI_API void SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0); // set named window collapsed state + IMGUI_API void SetWindowFocus(const char* name); // set named window to be focused / front-most. use NULL to remove focus. + + IMGUI_API float GetScrollX(); // get scrolling amount [0..GetScrollMaxX()] + IMGUI_API float GetScrollY(); // get scrolling amount [0..GetScrollMaxY()] + IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.X - WindowSize.X + IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.Y - WindowSize.Y + IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0..GetScrollMaxX()] + IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0..GetScrollMaxY()] + IMGUI_API void SetScrollHere(float center_y_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_y_ratio=0.0: top, 0.5: center, 1.0: bottom. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. + IMGUI_API void SetScrollFromPosY(float pos_y, float center_y_ratio = 0.5f); // adjust scrolling amount to make given position valid. use GetCursorPos() or GetCursorStartPos()+offset to get valid positions. + IMGUI_API void SetStateStorage(ImGuiStorage* tree); // replace tree state storage with our own (if you want to manipulate it yourself, typically clear subsection of it) + IMGUI_API ImGuiStorage* GetStateStorage(); + + // Parameters stacks (shared) + IMGUI_API void PushFont(ImFont* font); // use NULL as a shortcut to push default font + IMGUI_API void PopFont(); + IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); + IMGUI_API void PushStyleColor(ImGuiCol idx, const ImVec4& col); + IMGUI_API void PopStyleColor(int count = 1); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val); + IMGUI_API void PopStyleVar(int count = 1); + IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwhise use GetColorU32() to get style color + style alpha. + IMGUI_API ImFont* GetFont(); // get current font + IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied + IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a while pixel, useful to draw custom shapes via the ImDrawList API + IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier + IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied + IMGUI_API ImU32 GetColorU32(ImU32 col); // retrieve given color with style alpha applied + + // Parameters stacks (current window) + IMGUI_API void PushItemWidth(float item_width); // width of items for the common item+label case, pixels. 0.0f = default to ~2/3 of windows width, >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -1.0f always align width to the right side) + IMGUI_API void PopItemWidth(); + IMGUI_API float CalcItemWidth(); // width of item given pushed settings and current cursor position + IMGUI_API void PushTextWrapPos(float wrap_pos_x = 0.0f); // word-wrapping for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space + IMGUI_API void PopTextWrapPos(); + IMGUI_API void PushAllowKeyboardFocus(bool allow_keyboard_focus); // allow focusing using TAB/Shift-TAB, enabled by default but you can disable it for certain widgets + IMGUI_API void PopAllowKeyboardFocus(); + IMGUI_API void PushButtonRepeat(bool repeat); // in 'repeat' mode, Button*() functions return repeated true in a typematic manner (using io.KeyRepeatDelay/io.KeyRepeatRate setting). Note that you can call IsItemActive() after any Button() to tell if the button is held in the current frame. + IMGUI_API void PopButtonRepeat(); + + // Cursor / Layout + IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. + IMGUI_API void SameLine(float pos_x = 0.0f, float spacing_w = -1.0f); // call between widgets or groups to layout them horizontally + IMGUI_API void NewLine(); // undo a SameLine() + IMGUI_API void Spacing(); // add vertical spacing + IMGUI_API void Dummy(const ImVec2& size); // add a dummy item of given size + IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by style.IndentSpacing or indent_w if != 0 + IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by style.IndentSpacing or indent_w if != 0 + IMGUI_API void BeginGroup(); // lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) + IMGUI_API void EndGroup(); + IMGUI_API ImVec2 GetCursorPos(); // cursor position is relative to window position + IMGUI_API float GetCursorPosX(); // " + IMGUI_API float GetCursorPosY(); // " + IMGUI_API void SetCursorPos(const ImVec2& local_pos); // " + IMGUI_API void SetCursorPosX(float x); // " + IMGUI_API void SetCursorPosY(float y); // " + IMGUI_API ImVec2 GetCursorStartPos(); // initial cursor position + IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position in absolute screen coordinates [0..io.DisplaySize] (useful to work with ImDrawList API) + IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position in absolute screen coordinates [0..io.DisplaySize] + IMGUI_API void AlignTextToFramePadding(); // vertically align/lower upcoming text to FramePadding.y so that it will aligns to upcoming widgets (call if you have text on a line before regular widgets) + IMGUI_API float GetTextLineHeight(); // ~ FontSize + IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) + IMGUI_API float GetFrameHeight(); // ~ FontSize + style.FramePadding.y * 2 + IMGUI_API float GetFrameHeightWithSpacing(); // ~ FontSize + style.FramePadding.y * 2 + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of framed widgets) + + // Columns + // You can also use SameLine(pos_x) for simplified columns. The columns API is still work-in-progress and rather lacking. + IMGUI_API void Columns(int count = 1, const char* id = NULL, bool border = true); + IMGUI_API void NextColumn(); // next column, defaults to current row or next row if the current row is finished + IMGUI_API int GetColumnIndex(); // get current column index + IMGUI_API float GetColumnWidth(int column_index = -1); // get column width (in pixels). pass -1 to use current column + IMGUI_API void SetColumnWidth(int column_index, float width); // set column width (in pixels). pass -1 to use current column + IMGUI_API float GetColumnOffset(int column_index = -1); // get position of column line (in pixels, from the left side of the contents region). pass -1 to use current column, otherwise 0..GetColumnsCount() inclusive. column 0 is typically 0.0f + IMGUI_API void SetColumnOffset(int column_index, float offset_x); // set position of column line (in pixels, from the left side of the contents region). pass -1 to use current column + IMGUI_API int GetColumnsCount(); + + // ID scopes + // If you are creating widgets in a loop you most likely want to push a unique identifier (e.g. object pointer, loop index) so ImGui can differentiate them. + // You can also use the "##foobar" syntax within widget label to distinguish them from each others. Read "A primer on the use of labels/IDs" in the FAQ for more details. + IMGUI_API void PushID(const char* str_id); // push identifier into the ID stack. IDs are hash of the entire stack! + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); + IMGUI_API void PushID(const void* ptr_id); + IMGUI_API void PushID(int int_id); + IMGUI_API void PopID(); + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); + + // Widgets: Text + IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. + IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // simple formatted text + IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextColoredV(const ImVec4& col, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void TextDisabled(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); Text(fmt, ...); PopStyleColor(); + IMGUI_API void TextDisabledV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void TextWrapped(const char* fmt, ...) IM_FMTARGS(1); // shortcut for PushTextWrapPos(0.0f); Text(fmt, ...); PopTextWrapPos();. Note that this won't work on an auto-resizing window if there's no other widgets to extend the window width, yoy may need to set a size using SetNextWindowSize(). + IMGUI_API void TextWrappedV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void LabelText(const char* label, const char* fmt, ...) IM_FMTARGS(2); // display text+label aligned the same way as value+label widgets + IMGUI_API void LabelTextV(const char* label, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API void BulletText(const char* fmt, ...) IM_FMTARGS(1); // shortcut for Bullet()+Text() + IMGUI_API void BulletTextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void Bullet(); // draw a small circle and keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses + + // Widgets: Main + IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0,0)); // button + IMGUI_API bool SmallButton(const char* label); // button with FramePadding=(0,0) to easily embed within text + IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size); // button behavior without the visuals, useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0,0), const ImVec2& uv1 = ImVec2(1,1), const ImVec4& tint_col = ImVec4(1,1,1,1), const ImVec4& border_col = ImVec4(0,0,0,0)); + IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0,0), const ImVec2& uv1 = ImVec2(1,1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0,0,0,0), const ImVec4& tint_col = ImVec4(1,1,1,1)); // <0 frame_padding uses default frame padding settings. 0 for no padding + IMGUI_API bool Checkbox(const char* label, bool* v); + IMGUI_API bool CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value); + IMGUI_API bool RadioButton(const char* label, bool active); + IMGUI_API bool RadioButton(const char* label, int* v, int v_button); + IMGUI_API void PlotLines(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0,0), int stride = sizeof(float)); + IMGUI_API void PlotLines(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0,0)); + IMGUI_API void PlotHistogram(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0,0), int stride = sizeof(float)); + IMGUI_API void PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0,0)); + IMGUI_API void ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-1,0), const char* overlay = NULL); + + // Widgets: Combo Box + // The new BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it. + // The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. + IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); + IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! + IMGUI_API bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" + IMGUI_API bool Combo(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int popup_max_height_in_items = -1); + + // Widgets: Drags (tip: ctrl+click on a drag box to input with keyboard. manually input values aren't clamped, can go off-bounds) + // For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every functions, note that a 'float v[X]' function argument is the same as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x + // Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For gamepad/keyboard navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). + IMGUI_API bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", float power = 1.0f); // If v_min >= v_max we have no bound + IMGUI_API bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* display_format = "%.3f", const char* display_format_max = NULL, float power = 1.0f); + IMGUI_API bool DragInt(const char* label, int* v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f"); // If v_min >= v_max we have no bound + IMGUI_API bool DragInt2(const char* label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f"); + IMGUI_API bool DragInt3(const char* label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f"); + IMGUI_API bool DragInt4(const char* label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f"); + IMGUI_API bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* display_format = "%.0f", const char* display_format_max = NULL); + + // Widgets: Input with Keyboard + IMGUI_API bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiTextEditCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size = ImVec2(0,0), ImGuiInputTextFlags flags = 0, ImGuiTextEditCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputFloat(const char* label, float* v, float step = 0.0f, float step_fast = 0.0f, int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputFloat2(const char* label, float v[2], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputFloat3(const char* label, float v[3], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputFloat4(const char* label, float v[4], int decimal_precision = -1, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputInt(const char* label, int* v, int step = 1, int step_fast = 100, ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputInt2(const char* label, int v[2], ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputInt3(const char* label, int v[3], ImGuiInputTextFlags extra_flags = 0); + IMGUI_API bool InputInt4(const char* label, int v[4], ImGuiInputTextFlags extra_flags = 0); + + // Widgets: Sliders (tip: ctrl+click on a slider to input with keyboard. manually input values aren't clamped, can go off-bounds) + IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); // adjust display_format to decorate the value with a prefix or a suffix for in-slider labels or unit display. Use power!=1.0 for logarithmic sliders + IMGUI_API bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool SliderAngle(const char* label, float* v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f); + IMGUI_API bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* display_format = "%.0f"); + IMGUI_API bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* display_format = "%.0f"); + IMGUI_API bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* display_format = "%.0f"); + IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* display_format = "%.0f"); + IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* display_format = "%.3f", float power = 1.0f); + IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* display_format = "%.0f"); + + // Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little colored preview square that can be left-clicked to open a picker, and right-clicked to open an option menu.) + // Note that a 'float v[X]' function argument is the same as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. You can the pass the address of a first float element out of a contiguous structure, e.g. &myvector.x + IMGUI_API bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); + IMGUI_API bool ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags = 0, const float* ref_col = NULL); + IMGUI_API bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0,0)); // display a colored square/button, hover for details, return true when pressed. + IMGUI_API void SetColorEditOptions(ImGuiColorEditFlags flags); // initialize current options (generally on application startup) if you want to select a default format, picker type, etc. User will be able to change many settings, unless you pass the _NoOptions flag to your calls. + + // Widgets: Trees + IMGUI_API bool TreeNode(const char* label); // if returning 'true' the node is open and the tree id is pushed into the id stack. user is responsible for calling TreePop(). + IMGUI_API bool TreeNode(const char* str_id, const char* fmt, ...) IM_FMTARGS(2); // read the FAQ about why and how to use ID. to align arbitrary text at the same level as a TreeNode() you can use Bullet(). + IMGUI_API bool TreeNode(const void* ptr_id, const char* fmt, ...) IM_FMTARGS(2); // " + IMGUI_API bool TreeNodeV(const char* str_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeV(const void* ptr_id, const char* fmt, va_list args) IM_FMTLIST(2); + IMGUI_API bool TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags = 0); + IMGUI_API bool TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); + IMGUI_API bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); + IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushId(). Already called by TreeNode() when returning true, but you can call Push/Pop yourself for layout purpose + IMGUI_API void TreePush(const void* ptr_id = NULL); // " + IMGUI_API void TreePop(); // ~ Unindent()+PopId() + IMGUI_API void TreeAdvanceToLabelPos(); // advance cursor x position by GetTreeNodeToLabelSpacing() + IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode + IMGUI_API void SetNextTreeNodeOpen(bool is_open, ImGuiCond cond = 0); // set next TreeNode/CollapsingHeader open state. + IMGUI_API bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). + IMGUI_API bool CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags = 0); // when 'p_open' isn't NULL, display an additional small close button on upper right of the header + + // Widgets: Selectable / Lists + IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height + IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, bool (*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items = -1); + IMGUI_API bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0,0)); // use if you want to reimplement ListBox() will custom data or interactions. make sure to call ListBoxFooter() afterwards. + IMGUI_API bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1); // " + IMGUI_API void ListBoxFooter(); // terminate the scrolling region + + // Widgets: Value() Helpers. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) + IMGUI_API void Value(const char* prefix, bool b); + IMGUI_API void Value(const char* prefix, int v); + IMGUI_API void Value(const char* prefix, unsigned int v); + IMGUI_API void Value(const char* prefix, float v, const char* float_format = NULL); + + // Tooltips + IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set text tooltip under mouse-cursor, typically use with ImGui::IsItemHovered(). overidde any previous call to SetTooltip(). + IMGUI_API void SetTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void BeginTooltip(); // begin/append a tooltip window. to create full-featured tooltip (with any kind of contents). + IMGUI_API void EndTooltip(); + + // Menus + IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. + IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! + IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). + IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! + IMGUI_API bool BeginMenu(const char* label, bool enabled = true); // create a sub-menu entry. only call EndMenu() if this returns true! + IMGUI_API void EndMenu(); // only call EndBegin() if BeginMenu() returns true! + IMGUI_API bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. shortcuts are displayed for convenience but not processed by ImGui at the moment + IMGUI_API bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL + + // Popups + IMGUI_API void OpenPopup(const char* str_id); // call to mark popup as open (don't call every frame!). popups are closed when user click outside, or if CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. By default, Selectable()/MenuItem() are calling CloseCurrentPopup(). Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). + IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. only call EndPopup() if BeginPopup() returns true! + IMGUI_API bool BeginPopupContextItem(const char* str_id = NULL, int mouse_button = 1); // helper to open and begin popup when clicked on last item. if you can pass a NULL str_id only if the previous item had an id. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! + IMGUI_API bool BeginPopupContextWindow(const char* str_id = NULL, int mouse_button = 1, bool also_over_items = true); // helper to open and begin popup when clicked on current window. + IMGUI_API bool BeginPopupContextVoid(const char* str_id = NULL, int mouse_button = 1); // helper to open and begin popup when clicked in void (where there are no imgui windows). + IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // modal dialog (regular window with title bar, block interactions behind the modal window, can't close the modal window by clicking outside) + IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! + IMGUI_API bool OpenPopupOnItemClick(const char* str_id = NULL, int mouse_button = 1); // helper to open popup when clicked on last item. return true when just opened. + IMGUI_API bool IsPopupOpen(const char* str_id); // return true if the popup is open + IMGUI_API void CloseCurrentPopup(); // close the popup we have begin-ed into. clicking on a MenuItem or Selectable automatically close the current popup. + + // Logging/Capture: all text output from interface is captured to tty/file/clipboard. By default, tree nodes are automatically opened during logging. + IMGUI_API void LogToTTY(int max_depth = -1); // start logging to tty + IMGUI_API void LogToFile(int max_depth = -1, const char* filename = NULL); // start logging to file + IMGUI_API void LogToClipboard(int max_depth = -1); // start logging to OS clipboard + IMGUI_API void LogFinish(); // stop logging (close file, etc.) + IMGUI_API void LogButtons(); // helper to display buttons for logging to tty/file/clipboard + IMGUI_API void LogText(const char* fmt, ...) IM_FMTARGS(1); // pass text data straight to log (without being displayed) + + // Drag and Drop + // [BETA API] Missing Demo code. API may evolve. + IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call when the current item is active. If this return true, you can call SetDragDropPayload() + EndDragDropSource() + IMGUI_API bool SetDragDropPayload(const char* type, const void* data, size_t size, ImGuiCond cond = 0);// type is a user defined string of maximum 12 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. + IMGUI_API void EndDragDropSource(); // only call EndDragDropSource() if BeginDragDropSource() returns true! + IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive an item. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() + IMGUI_API const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. + IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! + + // Clipping + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); + IMGUI_API void PopClipRect(); + + // Focus, Activation + // (Prefer using "SetItemDefaultFocus()" over "if (IsWindowAppearing()) SetScrollHere()" when applicable, to make your code more forward compatible when navigation branch is merged) + IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a window. Please use instead of "if (IsWindowAppearing()) SetScrollHere()" to signify "default item". + IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. + + // Utilities + IMGUI_API bool IsItemHovered(ImGuiHoveredFlags flags = 0); // is the last item hovered? (and usable, aka not blocked by a popup, etc.). See ImGuiHoveredFlags for more options. + IMGUI_API bool IsItemActive(); // is the last item active? (e.g. button being held, text field being edited- items that don't interact will always return false) + IMGUI_API bool IsItemFocused(); // is the last item focused for keyboard/gamepad navigation? + IMGUI_API bool IsItemClicked(int mouse_button = 0); // is the last item clicked? (e.g. button/node just clicked on) + IMGUI_API bool IsItemVisible(); // is the last item visible? (aka not out of sight due to clipping/scrolling.) + IMGUI_API bool IsAnyItemHovered(); + IMGUI_API bool IsAnyItemActive(); + IMGUI_API bool IsAnyItemFocused(); + IMGUI_API ImVec2 GetItemRectMin(); // get bounding rectangle of last item, in screen space + IMGUI_API ImVec2 GetItemRectMax(); // " + IMGUI_API ImVec2 GetItemRectSize(); // get size of last item, in screen space + IMGUI_API void SetItemAllowOverlap(); // allow last item to be overlapped by a subsequent item. sometimes useful with invisible buttons, selectables, etc. to catch unused area. + IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags = 0); // is current window focused? or its root/child, depending on flags. see flags for options. + IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags = 0); // is current window hovered (and typically: not blocked by a popup/modal)? see flags for options. + IMGUI_API bool IsRectVisible(const ImVec2& size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. + IMGUI_API bool IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max); // test if rectangle (in screen space) is visible / not clipped. to perform coarse clipping on user's side. + IMGUI_API float GetTime(); + IMGUI_API int GetFrameCount(); + IMGUI_API ImDrawList* GetOverlayDrawList(); // this draw list will be the last rendered one, useful to quickly draw overlays shapes/text + IMGUI_API ImDrawListSharedData* GetDrawListSharedData(); + IMGUI_API const char* GetStyleColorName(ImGuiCol idx); + IMGUI_API ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); + IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // calculate coarse clipping for large list of evenly sized items. Prefer using the ImGuiListClipper higher-level helper if you can. + + IMGUI_API bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags = 0); // helper to create a child window / scrolling region that looks like a normal widget frame + IMGUI_API void EndChildFrame(); // always call EndChildFrame() regardless of BeginChildFrame() return values (which indicates a collapsed/clipped window) + + IMGUI_API ImVec4 ColorConvertU32ToFloat4(ImU32 in); + IMGUI_API ImU32 ColorConvertFloat4ToU32(const ImVec4& in); + IMGUI_API void ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v); + IMGUI_API void ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b); + + // Inputs + IMGUI_API int GetKeyIndex(ImGuiKey imgui_key); // map ImGuiKey_* values into user's key index. == io.KeyMap[key] + IMGUI_API bool IsKeyDown(int user_key_index); // is key being held. == io.KeysDown[user_key_index]. note that imgui doesn't know the semantic of each entry of io.KeyDown[]. Use your own indices/enums according to how your backend/engine stored them into KeyDown[]! + IMGUI_API bool IsKeyPressed(int user_key_index, bool repeat = true); // was key pressed (went from !Down to Down). if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate + IMGUI_API bool IsKeyReleased(int user_key_index); // was key released (went from Down to !Down).. + IMGUI_API int GetKeyPressedAmount(int key_index, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate + IMGUI_API bool IsMouseDown(int button); // is mouse button held + IMGUI_API bool IsAnyMouseDown(); // is any mouse button held + IMGUI_API bool IsMouseClicked(int button, bool repeat = false); // did mouse button clicked (went from !Down to Down) + IMGUI_API bool IsMouseDoubleClicked(int button); // did mouse button double-clicked. a double-click returns false in IsMouseClicked(). uses io.MouseDoubleClickTime. + IMGUI_API bool IsMouseReleased(int button); // did mouse button released (went from Down to !Down) + IMGUI_API bool IsMouseDragging(int button = 0, float lock_threshold = -1.0f); // is mouse dragging. if lock_threshold < -1.0f uses io.MouseDraggingThreshold + IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true); // is mouse hovering given bounding rect (in screen space). clipped by current clipping settings. disregarding of consideration of focus/window ordering/blocked by a popup. + IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // + IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls + IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve backup of mouse positioning at the time of opening popup we have BeginPopup() into + IMGUI_API ImVec2 GetMouseDragDelta(int button = 0, float lock_threshold = -1.0f); // dragging amount since clicking. if lock_threshold < -1.0f uses io.MouseDraggingThreshold + IMGUI_API void ResetMouseDragDelta(int button = 0); // + IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired cursor type, reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you + IMGUI_API void SetMouseCursor(ImGuiMouseCursor type); // set desired cursor type + IMGUI_API void CaptureKeyboardFromApp(bool capture = true); // manually override io.WantCaptureKeyboard flag next frame (said flag is entirely left for your application handle). e.g. force capture keyboard when your widget is being hovered. + IMGUI_API void CaptureMouseFromApp(bool capture = true); // manually override io.WantCaptureMouse flag next frame (said flag is entirely left for your application handle). + + // Clipboard Utilities (also see the LogToClipboard() function to capture or output text data to the clipboard) + IMGUI_API const char* GetClipboardText(); + IMGUI_API void SetClipboardText(const char* text); + + // Memory Utilities + // All those functions are not reliant on the current context. + // If you reload the contents of imgui.cpp at runtime, you may need to call SetCurrentContext() + SetAllocatorFunctions() again. + IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void(*free_func)(void* ptr, void* user_data), void* user_data = NULL); + IMGUI_API void* MemAlloc(size_t size); + IMGUI_API void MemFree(void* ptr); + +} // namespace ImGui + +// 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() +}; + +// 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() +}; + +// 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) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , 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 +}; + +// 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 +}; + +// 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 +}; + +// 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 +}; + +// Flags for ImGui::BeginDragDropSource(), ImGui::AcceptDragDropPayload() +enum ImGuiDragDropFlags_ +{ + // BeginDragDropSource() flags + ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disable this behavior. + ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return true, to avoid subsequent user code submitting tooltips. This flag disable this behavior so you can still call IsItemHovered() on the source item. + ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item. + ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit. + ImGuiDragDropFlags_SourceExtern = 1 << 4, // External source (from outside of imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously. + // AcceptDragDropPayload() flags + ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered. + ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, // Do not draw the default highlight rectangle when hovering over target. + ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect // For peeking ahead and inspecting the payload before delivery. +}; + +// Standard Drag and Drop payload types. You can define you own payload types using 12-characters long strings. Types starting with '_' are defined by Dear ImGui. +#define IMGUI_PAYLOAD_TYPE_COLOR_3F "_COL3F" // float[3] // Standard type for colors, without alpha. User code may use this type. +#define IMGUI_PAYLOAD_TYPE_COLOR_4F "_COL4F" // float[4] // Standard type for colors. User code may use this type. + +// User fill ImGuiIO.KeyMap[] array with indices into the ImGuiIO.KeysDown[512] array +enum ImGuiKey_ +{ + ImGuiKey_Tab, + ImGuiKey_LeftArrow, + ImGuiKey_RightArrow, + ImGuiKey_UpArrow, + ImGuiKey_DownArrow, + ImGuiKey_PageUp, + ImGuiKey_PageDown, + ImGuiKey_Home, + ImGuiKey_End, + ImGuiKey_Insert, + ImGuiKey_Delete, + ImGuiKey_Backspace, + ImGuiKey_Space, + ImGuiKey_Enter, + ImGuiKey_Escape, + ImGuiKey_A, // for text edit CTRL+A: select all + ImGuiKey_C, // for text edit CTRL+C: copy + ImGuiKey_V, // for text edit CTRL+V: paste + ImGuiKey_X, // for text edit CTRL+X: cut + ImGuiKey_Y, // for text edit CTRL+Y: redo + ImGuiKey_Z, // for text edit CTRL+Z: undo + ImGuiKey_COUNT +}; + +// [BETA] Gamepad/Keyboard directional navigation +// Keyboard: Set io.NavFlags |= ImGuiNavFlags_EnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.KeyDown[] + io.KeyMap[] arrays. +// Gamepad: Set io.NavFlags |= ImGuiNavFlags_EnableGamepad to enable. Fill the io.NavInputs[] fields before calling NewFrame(). Note that io.NavInputs[] is cleared by EndFrame(). +// Read instructions in imgui.cpp for more details. +enum ImGuiNavInput_ +{ + // Gamepad Mapping + ImGuiNavInput_Activate, // activate / open / toggle / tweak value // e.g. Circle (PS4), A (Xbox), B (Switch), Space (Keyboard) + ImGuiNavInput_Cancel, // cancel / close / exit // e.g. Cross (PS4), B (Xbox), A (Switch), Escape (Keyboard) + ImGuiNavInput_Input, // text input / on-screen keyboard // e.g. Triang.(PS4), Y (Xbox), X (Switch), Return (Keyboard) + ImGuiNavInput_Menu, // tap: toggle menu / hold: focus, move, resize // e.g. Square (PS4), X (Xbox), Y (Switch), Alt (Keyboard) + ImGuiNavInput_DpadLeft, // move / tweak / resize window (w/ PadMenu) // e.g. D-pad Left/Right/Up/Down (Gamepads), Arrow keys (Keyboard) + ImGuiNavInput_DpadRight, // + ImGuiNavInput_DpadUp, // + ImGuiNavInput_DpadDown, // + ImGuiNavInput_LStickLeft, // scroll / move window (w/ PadMenu) // e.g. Left Analog Stick Left/Right/Up/Down + ImGuiNavInput_LStickRight, // + ImGuiNavInput_LStickUp, // + ImGuiNavInput_LStickDown, // + ImGuiNavInput_FocusPrev, // next window (w/ PadMenu) // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) + ImGuiNavInput_FocusNext, // prev window (w/ PadMenu) // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) + ImGuiNavInput_TweakSlow, // slower tweaks // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) + ImGuiNavInput_TweakFast, // faster tweaks // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) + + // [Internal] Don't use directly! This is used internally to differentiate keyboard from gamepad inputs for behaviors that require to differentiate them. + // Keyboard behavior that have no corresponding gamepad mapping (e.g. CTRL+TAB) may be directly reading from io.KeyDown[] instead of io.NavInputs[]. + ImGuiNavInput_KeyMenu_, // toggle menu // = io.KeyAlt + ImGuiNavInput_KeyLeft_, // move left // = Arrow keys + ImGuiNavInput_KeyRight_, // move right + ImGuiNavInput_KeyUp_, // move up + ImGuiNavInput_KeyDown_, // move down + ImGuiNavInput_COUNT, + ImGuiNavInput_InternalStart_ = ImGuiNavInput_KeyMenu_ +}; + +// [BETA] Gamepad/Keyboard directional navigation flags, stored in io.NavFlags +enum ImGuiNavFlags_ +{ + ImGuiNavFlags_EnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. NewFrame() will automatically fill io.NavInputs[] based on io.KeyDown[]. + ImGuiNavFlags_EnableGamepad = 1 << 1, // Master gamepad navigation enable flag. This is mostly to instruct your imgui back-end to fill io.NavInputs[]. + ImGuiNavFlags_MoveMouse = 1 << 2, // Request navigation to allow moving the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. Will update io.MousePos and set io.WantMoveMouse=true. If enabled you MUST honor io.WantMoveMouse requests in your binding, otherwise ImGui will react as if the mouse is jumping around back and forth. + ImGuiNavFlags_NoCaptureKeyboard = 1 << 3 // Do not set the io.WantCaptureKeyboard flag with io.NavActive is set. +}; + +// Enumeration for PushStyleColor() / PopStyleColor() +enum ImGuiCol_ +{ + ImGuiCol_Text, + ImGuiCol_TextDisabled, + ImGuiCol_WindowBg, // Background of normal windows + ImGuiCol_ChildBg, // Background of child windows + ImGuiCol_PopupBg, // Background of popups, menus, tooltips windows + ImGuiCol_Border, + ImGuiCol_BorderShadow, + ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input + ImGuiCol_FrameBgHovered, + ImGuiCol_FrameBgActive, + ImGuiCol_TitleBg, + ImGuiCol_TitleBgActive, + ImGuiCol_TitleBgCollapsed, + ImGuiCol_MenuBarBg, + ImGuiCol_ScrollbarBg, + ImGuiCol_ScrollbarGrab, + ImGuiCol_ScrollbarGrabHovered, + ImGuiCol_ScrollbarGrabActive, + ImGuiCol_CheckMark, + ImGuiCol_SliderGrab, + ImGuiCol_SliderGrabActive, + ImGuiCol_Button, + ImGuiCol_ButtonHovered, + ImGuiCol_ButtonActive, + ImGuiCol_Header, + ImGuiCol_HeaderHovered, + ImGuiCol_HeaderActive, + ImGuiCol_Separator, + ImGuiCol_SeparatorHovered, + ImGuiCol_SeparatorActive, + ImGuiCol_ResizeGrip, + ImGuiCol_ResizeGripHovered, + ImGuiCol_ResizeGripActive, + ImGuiCol_CloseButton, + ImGuiCol_CloseButtonHovered, + ImGuiCol_CloseButtonActive, + ImGuiCol_PlotLines, + ImGuiCol_PlotLinesHovered, + ImGuiCol_PlotHistogram, + ImGuiCol_PlotHistogramHovered, + ImGuiCol_TextSelectedBg, + ImGuiCol_ModalWindowDarkening, // darken entire screen when a modal window is active + ImGuiCol_DragDropTarget, + ImGuiCol_NavHighlight, // gamepad/keyboard: current highlighted item + ImGuiCol_NavWindowingHighlight, // gamepad/keyboard: when holding NavMenu to focus/move/resize windows + ImGuiCol_COUNT + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //, ImGuiCol_ComboBg = ImGuiCol_PopupBg // ComboBg has been merged with PopupBg, so a redirect isn't accurate. + , ImGuiCol_ChildWindowBg = ImGuiCol_ChildBg, ImGuiCol_Column = ImGuiCol_Separator, ImGuiCol_ColumnHovered = ImGuiCol_SeparatorHovered, ImGuiCol_ColumnActive = ImGuiCol_SeparatorActive +#endif +}; + +// Enumeration for PushStyleVar() / PopStyleVar() to temporarily modify the ImGuiStyle structure. +// NB: the enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. During initialization, feel free to just poke into ImGuiStyle directly. +// NB: if changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. +enum ImGuiStyleVar_ +{ + // Enum name ......................// Member in ImGuiStyle structure (see ImGuiStyle for descriptions) + ImGuiStyleVar_Alpha, // float Alpha + ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding + ImGuiStyleVar_WindowRounding, // float WindowRounding + ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize + ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize + ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign + ImGuiStyleVar_ChildRounding, // float ChildRounding + ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize + ImGuiStyleVar_PopupRounding, // float PopupRounding + ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize + ImGuiStyleVar_FramePadding, // ImVec2 FramePadding + ImGuiStyleVar_FrameRounding, // float FrameRounding + ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize + ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing + ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing + ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize + ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding + ImGuiStyleVar_GrabMinSize, // float GrabMinSize + ImGuiStyleVar_GrabRounding, // float GrabRounding + ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_Count_ + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + , ImGuiStyleVar_ChildWindowRounding = ImGuiStyleVar_ChildRounding +#endif +}; + +// 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() +}; + +// 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_ +}; + +// 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) + + // 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 +#endif +}; + +// You may modify the ImGui::GetStyle() main instance during initialization and before NewFrame(). +// During the frame, prefer using ImGui::PushStyleVar(ImGuiStyleVar_XXXX)/PopStyleVar() to alter the main style values, and ImGui::PushStyleColor(ImGuiCol_XXX)/PopStyleColor() for colors. +struct ImGuiStyle +{ + 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. + +#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); +#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(); +}; + +//----------------------------------------------------------------------------- +// Obsolete functions (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) +//----------------------------------------------------------------------------- + +#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)); } +} +#endif + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +// Lightweight std::vector<> like class to avoid dragging dependencies (also: windows implementation of STL with debug enabled is absurdly slow, so let's bypass it so our code runs fast in debug). +// Our implementation does NOT call C++ constructors/destructors. This is intentional and we do not require it. Do not use this class as a straight std::vector replacement in your code! +template +class ImVector +{ +public: + int Size; + int Capacity; + T* Data; + + typedef T value_type; + typedef value_type* iterator; + typedef const value_type* const_iterator; + + inline ImVector() { Size = Capacity = 0; Data = NULL; } + inline ~ImVector() { if (Data) ImGui::MemFree(Data); } + + inline bool empty() const { return Size == 0; } + inline int size() const { return Size; } + inline int capacity() const { return Capacity; } + + inline value_type& operator[](int i) { IM_ASSERT(i < Size); return Data[i]; } + inline const value_type& operator[](int i) const { IM_ASSERT(i < Size); return Data[i]; } + + inline void clear() { if (Data) { Size = Capacity = 0; ImGui::MemFree(Data); Data = NULL; } } + inline iterator begin() { return Data; } + inline const_iterator begin() const { return Data; } + inline iterator end() { return Data + Size; } + inline const_iterator end() const { return Data + Size; } + inline value_type& front() { IM_ASSERT(Size > 0); return Data[0]; } + inline const value_type& front() const { IM_ASSERT(Size > 0); return Data[0]; } + inline value_type& back() { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline const value_type& back() const { IM_ASSERT(Size > 0); return Data[Size - 1]; } + inline void swap(ImVector& rhs) { int rhs_size = rhs.Size; rhs.Size = Size; Size = rhs_size; int rhs_cap = rhs.Capacity; rhs.Capacity = Capacity; Capacity = rhs_cap; value_type* rhs_data = rhs.Data; rhs.Data = Data; Data = rhs_data; } + + inline int _grow_capacity(int sz) const { int new_capacity = Capacity ? (Capacity + Capacity/2) : 8; return new_capacity > sz ? new_capacity : sz; } + + inline void resize(int new_size) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); Size = new_size; } + inline void resize(int new_size, const T& v){ if (new_size > Capacity) reserve(_grow_capacity(new_size)); if (new_size > Size) for (int n = Size; n < new_size; n++) Data[n] = v; Size = new_size; } + inline void reserve(int new_capacity) + { + if (new_capacity <= Capacity) + return; + T* new_data = (value_type*)ImGui::MemAlloc((size_t)new_capacity * sizeof(T)); + if (Data) + memcpy(new_data, Data, (size_t)Size * sizeof(T)); + ImGui::MemFree(Data); + Data = new_data; + Capacity = new_capacity; + } + + // NB: &v cannot be pointing inside the ImVector Data itself! e.g. v.push_back(v[10]) is forbidden. + inline void push_back(const value_type& v) { if (Size == Capacity) reserve(_grow_capacity(Size + 1)); Data[Size++] = v; } + inline void pop_back() { IM_ASSERT(Size > 0); Size--; } + inline void push_front(const value_type& v) { if (Size == 0) push_back(v); else insert(Data, v); } + + inline iterator erase(const_iterator it) { IM_ASSERT(it >= Data && it < Data+Size); const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + 1, ((size_t)Size - (size_t)off - 1) * sizeof(value_type)); Size--; return Data + off; } + inline iterator insert(const_iterator it, const value_type& v) { IM_ASSERT(it >= Data && it <= Data+Size); const ptrdiff_t off = it - Data; if (Size == Capacity) reserve(_grow_capacity(Size + 1)); if (off < (int)Size) memmove(Data + off + 1, Data + off, ((size_t)Size - (size_t)off) * sizeof(value_type)); Data[off] = v; Size++; return Data + off; } + inline bool contains(const value_type& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data++ == v) return true; return false; } +}; + +// Helper: execute a block of code at maximum once a frame. Convenient if you want to quickly create an UI within deep-nested code that runs multiple times every frame. +// Usage: +// static ImGuiOnceUponAFrame oaf; +// if (oaf) +// ImGui::Text("This will be called only once per frame"); +struct ImGuiOnceUponAFrame +{ + 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) +#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(); } +}; + +// 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); +}; + +// 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. +// 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). +// - You want to store custom debug data easily without adding or editing structures in your code (probably not efficient, but convenient) +// Types are NOT stored, so it is up to you to make sure your Key don't collide with different types. +struct ImGuiStorage +{ + struct Pair + { + ImGuiID key; + union { int val_i; float val_f; void* val_p; }; + Pair(ImGuiID _key, int _val_i) { key = _key; val_i = _val_i; } + Pair(ImGuiID _key, float _val_f) { key = _key; val_f = _val_f; } + Pair(ImGuiID _key, void* _val_p) { key = _key; val_p = _val_p; } + }; + ImVector Data; + + // - Get***() functions find pair, never add/allocate. Pairs are sorted so a query is O(log N) + // - Set***() functions find pair, insertion on demand if missing. + // - Sorted insertion is costly, paid once. A typical frame shouldn't need to insert any new pair. + void Clear() { Data.clear(); } + IMGUI_API int GetInt(ImGuiID key, int default_val = 0) const; + IMGUI_API void SetInt(ImGuiID key, int val); + IMGUI_API bool GetBool(ImGuiID key, bool default_val = false) const; + IMGUI_API void SetBool(ImGuiID key, bool val); + IMGUI_API float GetFloat(ImGuiID key, float default_val = 0.0f) const; + IMGUI_API void SetFloat(ImGuiID key, float val); + IMGUI_API void* GetVoidPtr(ImGuiID key) const; // default_val is NULL + IMGUI_API void SetVoidPtr(ImGuiID key, void* val); + + // - Get***Ref() functions finds pair, insert on demand if missing, return pointer. Useful if you intend to do Get+Set. + // - References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. + // - A typical use case where this is convenient for quick hacking (e.g. add storage during a live Edit&Continue session if you can't modify existing struct) + // float* pvar = ImGui::GetFloatRef(key); ImGui::SliderFloat("var", pvar, 0, 100.0f); some_var += *pvar; + IMGUI_API int* GetIntRef(ImGuiID key, int default_val = 0); + IMGUI_API bool* GetBoolRef(ImGuiID key, bool default_val = false); + IMGUI_API float* GetFloatRef(ImGuiID key, float default_val = 0.0f); + IMGUI_API void** GetVoidPtrRef(ImGuiID key, void* default_val = NULL); + + // Use on your own storage if you know only integer are being stored (open/close all tree nodes) + IMGUI_API void SetAllInt(int val); + + // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. + IMGUI_API void BuildSortByKey(); +}; + +// Shared state of InputText(), passed to callback when a ImGuiInputTextFlags_Callback* flag is used and the corresponding callback is triggered. +struct ImGuiTextEditCallbackData +{ + ImGuiInputTextFlags EventFlag; // One of ImGuiInputTextFlags_Callback* // Read-only + ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only + void* UserData; // What user passed to InputText() // Read-only + bool ReadOnly; // Read-only mode // Read-only + + // CharFilter event: + ImWchar EventChar; // Character input // Read-write (replace character or set to zero) + + // Completion,History,Always events: + // If you modify the buffer contents make sure you update 'BufTextLen' and set 'BufDirty' to true. + ImGuiKey EventKey; // Key pressed (Up/Down/TAB) // Read-only + char* Buf; // Current text buffer // Read-write (pointed data only, can't replace the actual pointer) + int BufTextLen; // Current text length in bytes // Read-write + int BufSize; // Maximum text length in bytes // Read-only + bool BufDirty; // Set if you modify Buf/BufTextLen!! // Write + int CursorPos; // // Read-write + int SelectionStart; // // Read-write (== to SelectionEnd when no selection) + int SelectionEnd; // // Read-write + + // NB: Helper functions for text manipulation. Calling those function loses selection. + IMGUI_API void DeleteChars(int pos, int bytes_count); + IMGUI_API void InsertChars(int pos, const char* text, const char* text_end = NULL); + bool HasSelection() const { return SelectionStart != SelectionEnd; } +}; + +// 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. +}; + +// Data payload for Drag and Drop operations +struct ImGuiPayload +{ + // Members + const void* Data; // Data (copied and owned by dear imgui) + int DataSize; // Data size + + // [Internal] + ImGuiID SourceId; // Source item id + ImGuiID SourceParentId; // Source parent id (if available) + int DataFrameCount; // Data timestamp + char DataType[12 + 1]; // Data type tag (short user-supplied string, 12 characters max) + bool Preview; // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets) + bool Delivery; // Set when AcceptDragDropPayload() was called and mouse button is released over the target item. + + ImGuiPayload() { Clear(); } + void Clear() { SourceId = SourceParentId = 0; Data = NULL; DataSize = 0; memset(DataType, 0, sizeof(DataType)); DataFrameCount = -1; Preview = Delivery = false; } + bool IsDataType(const char* type) const { return DataFrameCount != -1 && strcmp(type, DataType) == 0; } + bool IsPreview() const { return Preview; } + bool IsDelivery() const { return Delivery; } +}; + +// Helpers macros to generate 32-bits encoded colors +#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 +#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 +#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); } +}; + +// Helper: Manually clip large list of items. +// If you are submitting lots of evenly spaced items and you have a random access to the list, you can perform coarse clipping based on visibility to save yourself from processing those items at all. +// The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. +// ImGui already clip items based on their bounds but it needs to measure text size to do so. Coarse clipping before submission makes this cost and your own data fetching/submission cost null. +// Usage: +// ImGuiListClipper clipper(1000); // we have 1000 elements, evenly spaced. +// while (clipper.Step()) +// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) +// ImGui::Text("line number %d", i); +// - Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height (step skipped if we passed a known height as second arg to constructor). +// - Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element. +// - (Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user call Step(). Does nothing and switch to Step 3.) +// - Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop. +struct ImGuiListClipper +{ + 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. +}; + +//----------------------------------------------------------------------------- +// Draw List +// Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. +//----------------------------------------------------------------------------- + +// Draw callbacks for advanced uses. +// NB- You most likely do NOT need to use draw callbacks just to create your own widget or customized UI rendering (you can poke into the draw list for that) +// Draw callback may be useful for example, A) Change your GPU render state, B) render a complex 3D scene inside a UI element (without an intermediate texture/render target), etc. +// The expected behavior from your rendering function is 'if (cmd.UserCallback != NULL) cmd.UserCallback(parent_list, cmd); else RenderTriangles()' +typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd); + +// 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; } +}; + +// Vertex index (override with '#define ImDrawIdx unsigned int' inside in imconfig.h) +#ifndef ImDrawIdx +typedef unsigned short ImDrawIdx; +#endif + +// Vertex layout +#ifndef IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT +struct ImDrawVert +{ + ImVec2 pos; + ImVec2 uv; + ImU32 col; +}; +#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. +IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT; +#endif + +// Draw channels are used by the Columns API to "split" the render list into different channels while building, so items of each column can be batched together. +// You can also use them to simulate drawing layers and submit primitives in a different order than how they will be rendered. +struct ImDrawChannel +{ + 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 +}; + +enum ImDrawListFlags_ +{ + ImDrawListFlags_AntiAliasedLines = 1 << 0, + ImDrawListFlags_AntiAliasedFill = 1 << 1 +}; + +// Draw command list +// This is the low-level list of polygons that ImGui functions are filling. At the end of the frame, all command lists are passed to your ImGuiIO::RenderDrawListFn function for rendering. +// Each ImGui window contains its own ImDrawList. You can use ImGui::GetWindowDrawList() to access the current window draw list and draw custom primitives. +// You can interleave normal ImGui:: calls and adding primitives to the current draw list. +// All positions are generally in pixel coordinates (top-left at (0,0), bottom-right at io.DisplaySize), however you are totally free to apply whatever transformation matrix to want to the data (if you apply such transformation you'll want to apply it to ClipRect as well) +// 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(); +}; + +// 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. +}; + +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(); +}; + +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 +}; + +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 +}; + +// Load and rasterize multiple TTF/OTF fonts into a same texture. +// Sharing a texture for multiple fonts allows us to reduce the number of draw calls during rendering. +// We also add custom graphic data into the texture that serves for ImGui. +// 1. (Optional) Call AddFont*** functions. If you don't call any, the default font will be loaded for you. +// 2. Call GetTexDataAsAlpha8() or GetTexDataAsRGBA32() to build and retrieve pixels data. +// 3. Upload the pixels data into a texture within your graphics system. +// 4. Call SetTexID(my_tex_id); and pass the pointer/identifier to your texture. This value will be passed back to you during rendering to identify the texture. +// IMPORTANT: If you pass a 'glyph_ranges' array to AddFont*** functions, you need to make sure that your array persist up until the ImFont is build (when calling GetTextData*** or Build()). We only copy the pointer, not the data. +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 +}; + +// 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. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + typedef ImFontGlyph Glyph; // OBSOLETE 1.52+ +#endif +}; + +#if defined(__clang__) +#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" +#endif diff --git a/attachments/simple_engine/imgui/imgui_draw.cpp b/attachments/simple_engine/imgui/imgui_draw.cpp new file mode 100644 index 00000000..9ba63056 --- /dev/null +++ b/attachments/simple_engine/imgui/imgui_draw.cpp @@ -0,0 +1,2943 @@ +// dear imgui, v1.60 WIP +// (drawing and font code) + +// Contains implementation for +// - Default styles +// - ImDrawList +// - ImDrawData +// - ImFontAtlas +// - ImFont +// - Default font data + +#if defined(_MSC_VER) && !defined(_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 +#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 +#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 +#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 +#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 +#endif + +//------------------------------------------------------------------------- +// STB libraries 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 +{ +#endif + +#ifdef _MSC_VER +#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" +#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] +#endif + +#define STBRP_ASSERT(x) IM_ASSERT(x) +#ifndef IMGUI_DISABLE_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) +#ifndef IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +#define STBTT_STATIC +#define STB_TRUETYPE_IMPLEMENTATION +#else +#define STBTT_DEF extern +#endif +#include "stb_truetype.h" + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +#ifdef IMGUI_STB_NAMESPACE +} // namespace ImGuiStb +using namespace IMGUI_STB_NAMESPACE; +#endif + +//----------------------------------------------------------------------------- +// 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); +} + +// 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); +} + +//----------------------------------------------------------------------------- +// ImDrawListData +//----------------------------------------------------------------------------- + +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)); + } +} + +//----------------------------------------------------------------------------- +// ImDrawList +//----------------------------------------------------------------------------- + +void ImDrawList::Clear() +{ + CmdBuffer.resize(0); + IdxBuffer.resize(0); + VtxBuffer.resize(0); + Flags = ImDrawListFlags_AntiAliasedLines | ImDrawListFlags_AntiAliasedFill; + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.resize(0); + _TextureIdStack.resize(0); + _Path.resize(0); + _ChannelsCurrent = 0; + _ChannelsCount = 1; + // NB: Do not clear channels so our allocations are re-used after the first frame. +} + +void ImDrawList::ClearFreeMemory() +{ + CmdBuffer.clear(); + IdxBuffer.clear(); + VtxBuffer.clear(); + _VtxCurrentIdx = 0; + _VtxWritePtr = NULL; + _IdxWritePtr = NULL; + _ClipRectStack.clear(); + _TextureIdStack.clear(); + _Path.clear(); + _ChannelsCurrent = 0; + _ChannelsCount = 1; + for (int i = 0; i < _Channels.Size; i++) + { + if (i == 0) memset(&_Channels[0], 0, sizeof(_Channels[0])); // channel 0 is a copy of CmdBuffer/IdxBuffer, don't destruct again + _Channels[i].CmdBuffer.clear(); + _Channels[i].IdxBuffer.clear(); + } + _Channels.clear(); +} + +// Using macros because C++ is a terrible language, we want guaranteed inline, no code in header, and no overhead in Debug builds +#define GetCurrentClipRect() (_ClipRectStack.Size ? _ClipRectStack.Data[_ClipRectStack.Size-1] : _Data->ClipRectFullscreen) +#define GetCurrentTextureId() (_TextureIdStack.Size ? _TextureIdStack.Data[_TextureIdStack.Size-1] : NULL) + +void ImDrawList::AddDrawCmd() +{ + 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); +} + +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) +} + +// Our scheme may appears a bit unusual, basically we want the most-common calls AddLine AddRect etc. to not have to perform any check so we always have a command ready in the stack. +// The cost of figuring out if a new command has to be added or if we can merge is paid in those Update** functions only. +void ImDrawList::UpdateClipRect() +{ + // If current command is used with different settings we need to add a new command + const ImVec4 curr_clip_rect = GetCurrentClipRect(); + ImDrawCmd* curr_cmd = CmdBuffer.Size > 0 ? &CmdBuffer.Data[CmdBuffer.Size-1] : NULL; + if (!curr_cmd || (curr_cmd->ElemCount != 0 && memcmp(&curr_cmd->ClipRect, &curr_clip_rect, sizeof(ImVec4)) != 0) || curr_cmd->UserCallback != NULL) + { + AddDrawCmd(); + return; + } + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = CmdBuffer.Size > 1 ? curr_cmd - 1 : NULL; + if (curr_cmd->ElemCount == 0 && prev_cmd && memcmp(&prev_cmd->ClipRect, &curr_clip_rect, sizeof(ImVec4)) == 0 && prev_cmd->TextureId == GetCurrentTextureId() && prev_cmd->UserCallback == NULL) + CmdBuffer.pop_back(); + else + curr_cmd->ClipRect = curr_clip_rect; +} + +void ImDrawList::UpdateTextureID() +{ + // If current command is used with different settings we need to add a new command + const ImTextureID curr_texture_id = GetCurrentTextureId(); + ImDrawCmd* curr_cmd = CmdBuffer.Size ? &CmdBuffer.back() : NULL; + if (!curr_cmd || (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != curr_texture_id) || curr_cmd->UserCallback != NULL) + { + AddDrawCmd(); + return; + } + + // Try to merge with previous command if it matches, else use current command + ImDrawCmd* prev_cmd = CmdBuffer.Size > 1 ? curr_cmd - 1 : NULL; + if (curr_cmd->ElemCount == 0 && prev_cmd && prev_cmd->TextureId == curr_texture_id && memcmp(&prev_cmd->ClipRect, &GetCurrentClipRect(), sizeof(ImVec4)) == 0 && prev_cmd->UserCallback == NULL) + CmdBuffer.pop_back(); + else + curr_cmd->TextureId = curr_texture_id; +} + +#undef GetCurrentClipRect +#undef GetCurrentTextureId + +// Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) +void ImDrawList::PushClipRect(ImVec2 cr_min, ImVec2 cr_max, bool intersect_with_current_clip_rect) +{ + ImVec4 cr(cr_min.x, cr_min.y, cr_max.x, cr_max.y); + if (intersect_with_current_clip_rect && _ClipRectStack.Size) + { + ImVec4 current = _ClipRectStack.Data[_ClipRectStack.Size-1]; + if (cr.x < current.x) cr.x = current.x; + if (cr.y < current.y) cr.y = current.y; + if (cr.z > current.z) cr.z = current.z; + if (cr.w > current.w) cr.w = current.w; + } + cr.z = ImMax(cr.x, cr.z); + cr.w = ImMax(cr.y, cr.w); + + _ClipRectStack.push_back(cr); + UpdateClipRect(); +} + +void ImDrawList::PushClipRectFullScreen() +{ + 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(); +} + +void ImDrawList::PushTextureID(ImTextureID texture_id) +{ + _TextureIdStack.push_back(texture_id); + UpdateTextureID(); +} + +void ImDrawList::PopTextureID() +{ + IM_ASSERT(_TextureIdStack.Size > 0); + _TextureIdStack.pop_back(); + UpdateTextureID(); +} + +void ImDrawList::ChannelsSplit(int channels_count) +{ + IM_ASSERT(_ChannelsCurrent == 0 && _ChannelsCount == 1); + int old_channels_count = _Channels.Size; + if (old_channels_count < channels_count) + _Channels.resize(channels_count); + _ChannelsCount = channels_count; + + // _Channels[] (24/32 bytes each) hold storage that we'll swap with this->_CmdBuffer/_IdxBuffer + // The content of _Channels[0] at this point doesn't matter. We clear it to make state tidy in a debugger but we don't strictly need to. + // When we switch to the next channel, we'll copy _CmdBuffer/_IdxBuffer into _Channels[0] and then _Channels[1] into _CmdBuffer/_IdxBuffer + memset(&_Channels[0], 0, sizeof(ImDrawChannel)); + for (int i = 1; i < channels_count; i++) + { + if (i >= old_channels_count) + { + IM_PLACEMENT_NEW(&_Channels[i]) ImDrawChannel(); + } + else + { + _Channels[i].CmdBuffer.resize(0); + _Channels[i].IdxBuffer.resize(0); + } + if (_Channels[i].CmdBuffer.Size == 0) + { + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = _ClipRectStack.back(); + draw_cmd.TextureId = _TextureIdStack.back(); + _Channels[i].CmdBuffer.push_back(draw_cmd); + } + } +} + +void ImDrawList::ChannelsMerge() +{ + // Note that we never use or rely on channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. + if (_ChannelsCount <= 1) + return; + + ChannelsSetCurrent(0); + if (CmdBuffer.Size && CmdBuffer.back().ElemCount == 0) + CmdBuffer.pop_back(); + + int new_cmd_buffer_count = 0, new_idx_buffer_count = 0; + for (int i = 1; i < _ChannelsCount; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (ch.CmdBuffer.Size && ch.CmdBuffer.back().ElemCount == 0) + ch.CmdBuffer.pop_back(); + new_cmd_buffer_count += ch.CmdBuffer.Size; + new_idx_buffer_count += ch.IdxBuffer.Size; + } + CmdBuffer.resize(CmdBuffer.Size + new_cmd_buffer_count); + IdxBuffer.resize(IdxBuffer.Size + new_idx_buffer_count); + + ImDrawCmd* cmd_write = CmdBuffer.Data + CmdBuffer.Size - new_cmd_buffer_count; + _IdxWritePtr = IdxBuffer.Data + IdxBuffer.Size - new_idx_buffer_count; + for (int i = 1; i < _ChannelsCount; i++) + { + ImDrawChannel& ch = _Channels[i]; + if (int sz = ch.CmdBuffer.Size) { memcpy(cmd_write, ch.CmdBuffer.Data, sz * sizeof(ImDrawCmd)); cmd_write += sz; } + if (int sz = ch.IdxBuffer.Size) { memcpy(_IdxWritePtr, ch.IdxBuffer.Data, sz * sizeof(ImDrawIdx)); _IdxWritePtr += sz; } + } + UpdateClipRect(); // We call this instead of AddDrawCmd(), so that empty channels won't produce an extra draw call. + _ChannelsCount = 1; +} + +void ImDrawList::ChannelsSetCurrent(int idx) +{ + IM_ASSERT(idx < _ChannelsCount); + if (_ChannelsCurrent == idx) return; + memcpy(&_Channels.Data[_ChannelsCurrent].CmdBuffer, &CmdBuffer, sizeof(CmdBuffer)); // copy 12 bytes, four times + memcpy(&_Channels.Data[_ChannelsCurrent].IdxBuffer, &IdxBuffer, sizeof(IdxBuffer)); + _ChannelsCurrent = idx; + memcpy(&CmdBuffer, &_Channels.Data[_ChannelsCurrent].CmdBuffer, sizeof(CmdBuffer)); + memcpy(&IdxBuffer, &_Channels.Data[_ChannelsCurrent].IdxBuffer, sizeof(IdxBuffer)); + _IdxWritePtr = IdxBuffer.Data + IdxBuffer.Size; +} + +// NB: this can be called with negative count for removing primitives (as long as the result does not underflow) +void ImDrawList::PrimReserve(int idx_count, int vtx_count) +{ + ImDrawCmd& draw_cmd = CmdBuffer.Data[CmdBuffer.Size-1]; + draw_cmd.ElemCount += idx_count; + + int vtx_buffer_old_size = VtxBuffer.Size; + VtxBuffer.resize(vtx_buffer_old_size + vtx_count); + _VtxWritePtr = VtxBuffer.Data + vtx_buffer_old_size; + + int idx_buffer_old_size = IdxBuffer.Size; + IdxBuffer.resize(idx_buffer_old_size + idx_count); + _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; +} + +// 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; +} + +// TODO: Thickness anti-aliased lines cap are missing their AA fringe. +void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, bool closed, float thickness) +{ + if (points_count < 2) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + + int count = points_count; + if (!closed) + count = points_count-1; + + const bool thick_line = thickness > 1.0f; + if (Flags & ImDrawListFlags_AntiAliasedLines) + { + // Anti-aliased stroke + const float AA_SIZE = 1.0f; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + + const int idx_count = thick_line ? count*18 : count*12; + const int vtx_count = thick_line ? points_count*4 : points_count*3; + PrimReserve(idx_count, vtx_count); + + // Temporary buffer + ImVec2* temp_normals = (ImVec2*)alloca(points_count * (thick_line ? 5 : 3) * sizeof(ImVec2)); + ImVec2* temp_points = temp_normals + points_count; + + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1+1) == points_count ? 0 : i1+1; + ImVec2 diff = points[i2] - points[i1]; + diff *= ImInvLength(diff, 1.0f); + temp_normals[i1].x = diff.y; + temp_normals[i1].y = -diff.x; + } + if (!closed) + temp_normals[points_count-1] = temp_normals[points_count-2]; + + if (!thick_line) + { + if (!closed) + { + temp_points[0] = points[0] + temp_normals[0] * AA_SIZE; + temp_points[1] = points[0] - temp_normals[0] * AA_SIZE; + temp_points[(points_count-1)*2+0] = points[points_count-1] + temp_normals[points_count-1] * AA_SIZE; + temp_points[(points_count-1)*2+1] = points[points_count-1] - temp_normals[points_count-1] * AA_SIZE; + } + + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1+1) == points_count ? 0 : i1+1; + unsigned int idx2 = (i1+1) == points_count ? _VtxCurrentIdx : idx1+3; + + // Average normals + ImVec2 dm = (temp_normals[i1] + temp_normals[i2]) * 0.5f; + float dmr2 = dm.x*dm.x + dm.y*dm.y; + if (dmr2 > 0.000001f) + { + float scale = 1.0f / dmr2; + if (scale > 100.0f) scale = 100.0f; + dm *= scale; + } + dm *= AA_SIZE; + temp_points[i2*2+0] = points[i2] + dm; + temp_points[i2*2+1] = points[i2] - dm; + + // Add indexes + _IdxWritePtr[0] = (ImDrawIdx)(idx2+0); _IdxWritePtr[1] = (ImDrawIdx)(idx1+0); _IdxWritePtr[2] = (ImDrawIdx)(idx1+2); + _IdxWritePtr[3] = (ImDrawIdx)(idx1+2); _IdxWritePtr[4] = (ImDrawIdx)(idx2+2); _IdxWritePtr[5] = (ImDrawIdx)(idx2+0); + _IdxWritePtr[6] = (ImDrawIdx)(idx2+1); _IdxWritePtr[7] = (ImDrawIdx)(idx1+1); _IdxWritePtr[8] = (ImDrawIdx)(idx1+0); + _IdxWritePtr[9] = (ImDrawIdx)(idx1+0); _IdxWritePtr[10]= (ImDrawIdx)(idx2+0); _IdxWritePtr[11]= (ImDrawIdx)(idx2+1); + _IdxWritePtr += 12; + + idx1 = idx2; + } + + // Add vertexes + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos = temp_points[i*2+0]; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; + _VtxWritePtr[2].pos = temp_points[i*2+1]; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col_trans; + _VtxWritePtr += 3; + } + } + else + { + const float half_inner_thickness = (thickness - AA_SIZE) * 0.5f; + if (!closed) + { + temp_points[0] = points[0] + temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[1] = points[0] + temp_normals[0] * (half_inner_thickness); + temp_points[2] = points[0] - temp_normals[0] * (half_inner_thickness); + temp_points[3] = points[0] - temp_normals[0] * (half_inner_thickness + AA_SIZE); + temp_points[(points_count-1)*4+0] = points[points_count-1] + temp_normals[points_count-1] * (half_inner_thickness + AA_SIZE); + temp_points[(points_count-1)*4+1] = points[points_count-1] + temp_normals[points_count-1] * (half_inner_thickness); + temp_points[(points_count-1)*4+2] = points[points_count-1] - temp_normals[points_count-1] * (half_inner_thickness); + temp_points[(points_count-1)*4+3] = points[points_count-1] - temp_normals[points_count-1] * (half_inner_thickness + AA_SIZE); + } + + // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. + unsigned int idx1 = _VtxCurrentIdx; + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1+1) == points_count ? 0 : i1+1; + unsigned int idx2 = (i1+1) == points_count ? _VtxCurrentIdx : idx1+4; + + // Average normals + ImVec2 dm = (temp_normals[i1] + temp_normals[i2]) * 0.5f; + float dmr2 = dm.x*dm.x + dm.y*dm.y; + if (dmr2 > 0.000001f) + { + float scale = 1.0f / dmr2; + if (scale > 100.0f) scale = 100.0f; + dm *= scale; + } + ImVec2 dm_out = dm * (half_inner_thickness + AA_SIZE); + ImVec2 dm_in = dm * half_inner_thickness; + temp_points[i2*4+0] = points[i2] + dm_out; + temp_points[i2*4+1] = points[i2] + dm_in; + temp_points[i2*4+2] = points[i2] - dm_in; + temp_points[i2*4+3] = points[i2] - dm_out; + + // Add indexes + _IdxWritePtr[0] = (ImDrawIdx)(idx2+1); _IdxWritePtr[1] = (ImDrawIdx)(idx1+1); _IdxWritePtr[2] = (ImDrawIdx)(idx1+2); + _IdxWritePtr[3] = (ImDrawIdx)(idx1+2); _IdxWritePtr[4] = (ImDrawIdx)(idx2+2); _IdxWritePtr[5] = (ImDrawIdx)(idx2+1); + _IdxWritePtr[6] = (ImDrawIdx)(idx2+1); _IdxWritePtr[7] = (ImDrawIdx)(idx1+1); _IdxWritePtr[8] = (ImDrawIdx)(idx1+0); + _IdxWritePtr[9] = (ImDrawIdx)(idx1+0); _IdxWritePtr[10] = (ImDrawIdx)(idx2+0); _IdxWritePtr[11] = (ImDrawIdx)(idx2+1); + _IdxWritePtr[12] = (ImDrawIdx)(idx2+2); _IdxWritePtr[13] = (ImDrawIdx)(idx1+2); _IdxWritePtr[14] = (ImDrawIdx)(idx1+3); + _IdxWritePtr[15] = (ImDrawIdx)(idx1+3); _IdxWritePtr[16] = (ImDrawIdx)(idx2+3); _IdxWritePtr[17] = (ImDrawIdx)(idx2+2); + _IdxWritePtr += 18; + + idx1 = idx2; + } + + // Add vertexes + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i*4+0]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col_trans; + _VtxWritePtr[1].pos = temp_points[i*4+1]; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = temp_points[i*4+2]; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = temp_points[i*4+3]; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col_trans; + _VtxWritePtr += 4; + } + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Stroke + const int idx_count = count*6; + const int vtx_count = count*4; // FIXME-OPT: Not sharing edges + PrimReserve(idx_count, vtx_count); + + for (int i1 = 0; i1 < count; i1++) + { + const int i2 = (i1+1) == points_count ? 0 : i1+1; + const ImVec2& p1 = points[i1]; + const ImVec2& p2 = points[i2]; + ImVec2 diff = p2 - p1; + diff *= ImInvLength(diff, 1.0f); + + const float dx = diff.x * (thickness * 0.5f); + const float dy = diff.y * (thickness * 0.5f); + _VtxWritePtr[0].pos.x = p1.x + dy; _VtxWritePtr[0].pos.y = p1.y - dx; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos.x = p2.x + dy; _VtxWritePtr[1].pos.y = p2.y - dx; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos.x = p2.x - dy; _VtxWritePtr[2].pos.y = p2.y + dx; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos.x = p1.x - dy; _VtxWritePtr[3].pos.y = p1.y + dx; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col; + _VtxWritePtr += 4; + + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx+1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx+2); + _IdxWritePtr[3] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[4] = (ImDrawIdx)(_VtxCurrentIdx+2); _IdxWritePtr[5] = (ImDrawIdx)(_VtxCurrentIdx+3); + _IdxWritePtr += 6; + _VtxCurrentIdx += 4; + } + } +} + +void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + const ImVec2 uv = _Data->TexUvWhitePixel; + + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = 1.0f; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count-2)*3 + points_count*6; + const int vtx_count = (points_count*2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx+1; + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx+((i-1)<<1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx+(i<<1)); + _IdxWritePtr += 3; + } + + // Compute normals + ImVec2* temp_normals = (ImVec2*)alloca(points_count * sizeof(ImVec2)); + for (int i0 = points_count-1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + ImVec2 diff = p1 - p0; + diff *= ImInvLength(diff, 1.0f); + temp_normals[i0].x = diff.y; + temp_normals[i0].y = -diff.x; + } + + for (int i0 = points_count-1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + ImVec2 dm = (n0 + n1) * 0.5f; + float dmr2 = dm.x*dm.x + dm.y*dm.y; + if (dmr2 > 0.000001f) + { + float scale = 1.0f / dmr2; + if (scale > 100.0f) scale = 100.0f; + dm *= scale; + } + dm *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos = (points[i1] - dm); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos = (points[i1] + dm); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx+(i1<<1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx+(i0<<1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx+(i0<<1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx+(i0<<1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx+(i1<<1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx+(i1<<1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count-2)*3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx+i-1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx+i); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} + +void ImDrawList::PathArcToFast(const ImVec2& 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)); + } +} + +void ImDrawList::PathArcTo(const ImVec2& centre, float radius, float a_min, float a_max, int num_segments) +{ + if (radius == 0.0f) + { + _Path.push_back(centre); + return; + } + _Path.reserve(_Path.Size + (num_segments + 1)); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + _Path.push_back(ImVec2(centre.x + cosf(a) * radius, centre.y + sinf(a) * radius)); + } +} + +static void PathBezierToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +{ + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = ((x2 - x4) * dy - (y2 - y4) * dx); + float d3 = ((x3 - x4) * dy - (y3 - y4) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2+d3) * (d2+d3) < tess_tol * (dx*dx + dy*dy)) + { + path->push_back(ImVec2(x4, y4)); + } + else if (level < 10) + { + float x12 = (x1+x2)*0.5f, y12 = (y1+y2)*0.5f; + float x23 = (x2+x3)*0.5f, y23 = (y2+y3)*0.5f; + float x34 = (x3+x4)*0.5f, y34 = (y3+y4)*0.5f; + float x123 = (x12+x23)*0.5f, y123 = (y12+y23)*0.5f; + float x234 = (x23+x34)*0.5f, y234 = (y23+y34)*0.5f; + float x1234 = (x123+x234)*0.5f, y1234 = (y123+y234)*0.5f; + + PathBezierToCasteljau(path, x1,y1, x12,y12, x123,y123, x1234,y1234, tess_tol, level+1); + PathBezierToCasteljau(path, x1234,y1234, x234,y234, x34,y34, x4,y4, tess_tol, level+1); + } +} + +void ImDrawList::PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) +{ + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + // Auto-tessellated + PathBezierToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, _Data->CurveTessellationTol, 0); + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + { + float t = t_step * i_step; + float u = 1.0f - t; + float w1 = u*u*u; + float w2 = 3*u*u*t; + float w3 = 3*u*t*t; + float w4 = t*t*t; + _Path.push_back(ImVec2(w1*p1.x + w2*p2.x + w3*p3.x + w4*p4.x, w1*p1.y + w2*p2.y + w3*p3.y + w4*p4.y)); + } + } +} + +void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, int rounding_corners) +{ + rounding = ImMin(rounding, fabsf(b.x - a.x) * ( ((rounding_corners & ImDrawCornerFlags_Top) == ImDrawCornerFlags_Top) || ((rounding_corners & ImDrawCornerFlags_Bot) == ImDrawCornerFlags_Bot) ? 0.5f : 1.0f ) - 1.0f); + rounding = ImMin(rounding, fabsf(b.y - a.y) * ( ((rounding_corners & ImDrawCornerFlags_Left) == ImDrawCornerFlags_Left) || ((rounding_corners & ImDrawCornerFlags_Right) == ImDrawCornerFlags_Right) ? 0.5f : 1.0f ) - 1.0f); + + if (rounding <= 0.0f || rounding_corners == 0) + { + PathLineTo(a); + PathLineTo(ImVec2(b.x, a.y)); + PathLineTo(b); + PathLineTo(ImVec2(a.x, b.y)); + } + else + { + const float rounding_tl = (rounding_corners & ImDrawCornerFlags_TopLeft) ? rounding : 0.0f; + const float rounding_tr = (rounding_corners & ImDrawCornerFlags_TopRight) ? rounding : 0.0f; + const float rounding_br = (rounding_corners & ImDrawCornerFlags_BotRight) ? rounding : 0.0f; + const float rounding_bl = (rounding_corners & ImDrawCornerFlags_BotLeft) ? rounding : 0.0f; + PathArcToFast(ImVec2(a.x + rounding_tl, a.y + rounding_tl), rounding_tl, 6, 9); + PathArcToFast(ImVec2(b.x - rounding_tr, a.y + rounding_tr), rounding_tr, 9, 12); + PathArcToFast(ImVec2(b.x - rounding_br, b.y - rounding_br), rounding_br, 0, 3); + PathArcToFast(ImVec2(a.x + rounding_bl, b.y - rounding_bl), rounding_bl, 3, 6); + } +} + +void ImDrawList::AddLine(const ImVec2& a, const ImVec2& b, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + PathLineTo(a + ImVec2(0.5f,0.5f)); + PathLineTo(b + ImVec2(0.5f,0.5f)); + PathStroke(col, false, thickness); +} + +// a: upper-left, b: lower-right. we don't render 1 px sized rectangles properly. +void ImDrawList::AddRect(const ImVec2& a, const ImVec2& b, ImU32 col, float rounding, int rounding_corners_flags, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + if (Flags & ImDrawListFlags_AntiAliasedLines) + PathRect(a + ImVec2(0.5f,0.5f), b - ImVec2(0.50f,0.50f), rounding, rounding_corners_flags); + else + PathRect(a + ImVec2(0.5f,0.5f), b - ImVec2(0.49f,0.49f), rounding, rounding_corners_flags); // Better looking lower-right corner and rounded non-AA shapes. + PathStroke(col, true, thickness); +} + +void ImDrawList::AddRectFilled(const ImVec2& a, const ImVec2& b, ImU32 col, float rounding, int rounding_corners_flags) +{ + 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); + } +} + +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::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; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathLineTo(d); + PathStroke(col, true, thickness); +} + +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::AddTriangle(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathStroke(col, true, thickness); +} + +void ImDrawList::AddTriangleFilled(const ImVec2& a, const ImVec2& b, const ImVec2& c, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(a); + PathLineTo(b); + PathLineTo(c); + PathFillConvex(col); +} + +void ImDrawList::AddCircle(const ImVec2& centre, float radius, ImU32 col, int num_segments, float thickness) +{ + 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); +} + +void ImDrawList::AddCircleFilled(const ImVec2& centre, float radius, ImU32 col, int num_segments) +{ + 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); +} + +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; + + PathLineTo(pos0); + PathBezierCurveTo(cp0, cp1, pos1, num_segments); + PathStroke(col, false, thickness); +} + +void ImDrawList::AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + 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::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) +{ + AddText(NULL, 0.0f, pos, col, text_begin, text_end); +} + +void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col) +{ + 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(); +} + +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; + + 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); + + 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(); +} + +//----------------------------------------------------------------------------- +// ImDrawData +//----------------------------------------------------------------------------- + +// For backward compatibility: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! +void ImDrawData::DeIndexAllBuffers() +{ + ImVector new_vtx_buffer; + TotalVtxCount = TotalIdxCount = 0; + for (int i = 0; i < CmdListsCount; i++) + { + ImDrawList* cmd_list = CmdLists[i]; + if (cmd_list->IdxBuffer.empty()) + continue; + new_vtx_buffer.resize(cmd_list->IdxBuffer.Size); + for (int j = 0; j < cmd_list->IdxBuffer.Size; j++) + new_vtx_buffer[j] = cmd_list->VtxBuffer[cmd_list->IdxBuffer[j]]; + cmd_list->VtxBuffer.swap(new_vtx_buffer); + cmd_list->IdxBuffer.resize(0); + TotalVtxCount += cmd_list->VtxBuffer.Size; + } +} + +// Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. +void ImDrawData::ScaleClipRects(const ImVec2& scale) +{ + for (int i = 0; i < CmdListsCount; i++) + { + ImDrawList* cmd_list = CmdLists[i]; + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + { + ImDrawCmd* cmd = &cmd_list->CmdBuffer[cmd_i]; + cmd->ClipRect = ImVec4(cmd->ClipRect.x * scale.x, cmd->ClipRect.y * scale.y, cmd->ClipRect.z * scale.x, cmd->ClipRect.w * scale.y); + } + } +} + +//----------------------------------------------------------------------------- +// Shade functions +//----------------------------------------------------------------------------- + +// Generic linear color gradient, write to RGB fields, leave A untouched. +void ImGui::ShadeVertsLinearColorGradientKeepAlpha(ImDrawVert* vert_start, ImDrawVert* vert_end, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1) +{ + ImVec2 gradient_extent = gradient_p1 - gradient_p0; + float gradient_inv_length2 = 1.0f / ImLengthSqr(gradient_extent); + for (ImDrawVert* vert = vert_start; vert < vert_end; vert++) + { + float d = ImDot(vert->pos - gradient_p0, gradient_extent); + float t = ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); + int r = ImLerp((int)(col0 >> IM_COL32_R_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_R_SHIFT) & 0xFF, t); + int g = ImLerp((int)(col0 >> IM_COL32_G_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_G_SHIFT) & 0xFF, t); + int b = ImLerp((int)(col0 >> IM_COL32_B_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_B_SHIFT) & 0xFF, t); + vert->col = (r << IM_COL32_R_SHIFT) | (g << IM_COL32_G_SHIFT) | (b << IM_COL32_B_SHIFT) | (vert->col & IM_COL32_A_MASK); + } +} + +// Scan and shade backward from the end of given vertices. Assume vertices are text only (= vert_start..vert_end going left to right) so we can break as soon as we are out the gradient bounds. +void ImGui::ShadeVertsLinearAlphaGradientForLeftToRightText(ImDrawVert* vert_start, ImDrawVert* vert_end, float gradient_p0_x, float gradient_p1_x) +{ + float gradient_extent_x = gradient_p1_x - gradient_p0_x; + float gradient_inv_length2 = 1.0f / (gradient_extent_x * gradient_extent_x); + int full_alpha_count = 0; + for (ImDrawVert* vert = vert_end - 1; vert >= vert_start; vert--) + { + float d = (vert->pos.x - gradient_p0_x) * (gradient_extent_x); + float alpha_mul = 1.0f - ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); + if (alpha_mul >= 1.0f && ++full_alpha_count > 2) + return; // Early out + int a = (int)(((vert->col >> IM_COL32_A_SHIFT) & 0xFF) * alpha_mul); + vert->col = (vert->col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); + } +} + +// 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) +{ + 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); + } +} + +//----------------------------------------------------------------------------- +// ImFontConfig +//----------------------------------------------------------------------------- + +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; +} + +//----------------------------------------------------------------------------- +// ImFontAtlas +//----------------------------------------------------------------------------- + +// 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 - " +}; + +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 +}; + +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; +} + +ImFontAtlas::~ImFontAtlas() +{ + Clear(); +} + +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; +} + +void ImFontAtlas::ClearTexData() +{ + if (TexPixelsAlpha8) + ImGui::MemFree(TexPixelsAlpha8); + if (TexPixelsRGBA32) + ImGui::MemFree(TexPixelsRGBA32); + TexPixelsAlpha8 = NULL; + TexPixelsRGBA32 = NULL; +} + +void ImFontAtlas::ClearFonts() +{ + for (int i = 0; i < Fonts.Size; i++) + IM_DELETE(Fonts[i]); + Fonts.clear(); +} + +void ImFontAtlas::Clear() +{ + ClearInputData(); + ClearTexData(); + ClearFonts(); +} + +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; +} + +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); + } + + // 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) +{ + 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) +{ + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (!font_cfg_template) + { + font_cfg.OversampleH = font_cfg.OversampleV = 1; + font_cfg.PixelSnapH = true; + } + if (font_cfg.Name[0] == '\0') strcpy(font_cfg.Name, "ProggyClean.ttf, 13px"); + if (font_cfg.SizePixels <= 0.0f) font_cfg.SizePixels = 13.0f; + + const char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85(); + ImFont* font = AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, GetGlyphRangesDefault()); + return font; +} + +ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +{ + int data_size = 0; + void* data = ImFileLoadToMemory(filename, "rb", &data_size, 0); + if (!data) + { + IM_ASSERT(0); // Could not load file. + return NULL; + } + ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); + if (font_cfg.Name[0] == '\0') + { + // Store a short copy of filename into into the font name for convenience + const char* p; + for (p = filename + strlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) {} + snprintf(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s, %.0fpx", p, size_pixels); + } + return AddFontFromMemoryTTF(data, data_size, size_pixels, &font_cfg, glyph_ranges); +} + +// 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) +{ + 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) +{ + 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) +{ + 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 +} + +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 +} + +void ImFontAtlas::CalcCustomRectUV(const CustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) +{ + IM_ASSERT(TexWidth > 0 && TexHeight > 0); // Font atlas needs to be built before we can calculate UV coordinates + IM_ASSERT(rect->IsPacked()); // Make sure the rectangle has been packed + *out_uv_min = ImVec2((float)rect->X * TexUvScale.x, (float)rect->Y * TexUvScale.y); + *out_uv_max = ImVec2((float)(rect->X + rect->Width) * TexUvScale.x, (float)(rect->Y + rect->Height) * TexUvScale.y); +} + +bool ImFontAtlas::GetMouseCursorTexData(ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]) +{ + if (cursor_type <= ImGuiMouseCursor_None || cursor_type >= ImGuiMouseCursor_Count_) + return false; + if (Flags & ImFontAtlasFlags_NoMouseCursors) + return false; + + ImFontAtlas::CustomRect& r = CustomRects[CustomRectIds[0]]; + IM_ASSERT(r.ID == FONT_ATLAS_DEFAULT_TEX_DATA_ID); + ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r.X, (float)r.Y); + ImVec2 size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][1]; + *out_size = size; + *out_offset = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][2]; + out_uv_border[0] = (pos) * TexUvScale; + out_uv_border[1] = (pos + size) * TexUvScale; + pos.x += FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF + 1; + out_uv_fill[0] = (pos) * TexUvScale; + out_uv_fill[1] = (pos + size) * TexUvScale; + return true; +} + +bool ImFontAtlas::Build() +{ + return ImFontAtlasBuildWithStbTruetype(this); +} + +void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor) +{ + for (unsigned int i = 0; i < 256; i++) + { + unsigned int value = (unsigned int)(i * in_brighten_factor); + out_table[i] = value > 255 ? 255 : (value & 0xFF); + } +} + +void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride) +{ + unsigned char* data = pixels + x + y * stride; + for (int j = h; j > 0; j--, data += stride) + for (int i = 0; i < w; i++) + data[i] = table[data[i]]; +} + +bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) +{ + IM_ASSERT(atlas->ConfigData.Size > 0); + + ImFontAtlasBuildRegisterDefaultCustomRects(atlas); + + atlas->TexID = NULL; + atlas->TexWidth = atlas->TexHeight = 0; + atlas->TexUvScale = ImVec2(0.0f, 0.0f); + atlas->TexUvWhitePixel = ImVec2(0.0f, 0.0f); + atlas->ClearTexData(); + + // Count glyphs/ranges + int total_glyphs_count = 0; + int total_ranges_count = 0; + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + if (!cfg.GlyphRanges) + cfg.GlyphRanges = atlas->GetGlyphRangesDefault(); + for (const ImWchar* in_range = cfg.GlyphRanges; in_range[0] && in_range[1]; in_range += 2, total_ranges_count++) + total_glyphs_count += (in_range[1] - in_range[0]) + 1; + } + + // We need a width for the skyline algorithm. Using a dumb heuristic here to decide of width. User can override TexDesiredWidth and TexGlyphPadding if they wish. + // Width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height. + atlas->TexWidth = (atlas->TexDesiredWidth > 0) ? atlas->TexDesiredWidth : (total_glyphs_count > 4000) ? 4096 : (total_glyphs_count > 2000) ? 2048 : (total_glyphs_count > 1000) ? 1024 : 512; + atlas->TexHeight = 0; + + // Start packing + const int max_tex_height = 1024*32; + stbtt_pack_context spc = {}; + if (!stbtt_PackBegin(&spc, NULL, atlas->TexWidth, max_tex_height, 0, atlas->TexGlyphPadding, NULL)) + return false; + stbtt_PackSetOversampling(&spc, 1, 1); + + // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). + ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info); + + // Initialize font information (so we can error without any cleanup) + struct ImFontTempBuildData + { + stbtt_fontinfo FontInfo; + stbrp_rect* Rects; + int RectsCount; + stbtt_pack_range* Ranges; + int RangesCount; + }; + ImFontTempBuildData* tmp_array = (ImFontTempBuildData*)ImGui::MemAlloc((size_t)atlas->ConfigData.Size * sizeof(ImFontTempBuildData)); + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + ImFontTempBuildData& tmp = tmp_array[input_i]; + IM_ASSERT(cfg.DstFont && (!cfg.DstFont->IsLoaded() || cfg.DstFont->ContainerAtlas == atlas)); + + const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)cfg.FontData, cfg.FontNo); + IM_ASSERT(font_offset >= 0); + if (!stbtt_InitFont(&tmp.FontInfo, (unsigned char*)cfg.FontData, font_offset)) + { + atlas->TexWidth = atlas->TexHeight = 0; // Reset output on failure + ImGui::MemFree(tmp_array); + return false; + } + } + + // Allocate packing character data and flag packed characters buffer as non-packed (x0=y0=x1=y1=0) + int buf_packedchars_n = 0, buf_rects_n = 0, buf_ranges_n = 0; + stbtt_packedchar* buf_packedchars = (stbtt_packedchar*)ImGui::MemAlloc(total_glyphs_count * sizeof(stbtt_packedchar)); + stbrp_rect* buf_rects = (stbrp_rect*)ImGui::MemAlloc(total_glyphs_count * sizeof(stbrp_rect)); + stbtt_pack_range* buf_ranges = (stbtt_pack_range*)ImGui::MemAlloc(total_ranges_count * sizeof(stbtt_pack_range)); + memset(buf_packedchars, 0, total_glyphs_count * sizeof(stbtt_packedchar)); + memset(buf_rects, 0, total_glyphs_count * sizeof(stbrp_rect)); // Unnecessary but let's clear this for the sake of sanity. + memset(buf_ranges, 0, total_ranges_count * sizeof(stbtt_pack_range)); + + // First font pass: pack all glyphs (no rendering at this point, we are working with rectangles in an infinitely tall texture at this point) + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + ImFontTempBuildData& tmp = tmp_array[input_i]; + + // Setup ranges + int font_glyphs_count = 0; + int font_ranges_count = 0; + for (const ImWchar* in_range = cfg.GlyphRanges; in_range[0] && in_range[1]; in_range += 2, font_ranges_count++) + font_glyphs_count += (in_range[1] - in_range[0]) + 1; + tmp.Ranges = buf_ranges + buf_ranges_n; + tmp.RangesCount = font_ranges_count; + buf_ranges_n += font_ranges_count; + for (int i = 0; i < font_ranges_count; i++) + { + const ImWchar* in_range = &cfg.GlyphRanges[i * 2]; + stbtt_pack_range& range = tmp.Ranges[i]; + range.font_size = cfg.SizePixels; + range.first_unicode_codepoint_in_range = in_range[0]; + range.num_chars = (in_range[1] - in_range[0]) + 1; + range.chardata_for_range = buf_packedchars + buf_packedchars_n; + buf_packedchars_n += range.num_chars; + } + + // Pack + tmp.Rects = buf_rects + buf_rects_n; + tmp.RectsCount = font_glyphs_count; + buf_rects_n += font_glyphs_count; + stbtt_PackSetOversampling(&spc, cfg.OversampleH, cfg.OversampleV); + int n = stbtt_PackFontRangesGatherRects(&spc, &tmp.FontInfo, tmp.Ranges, tmp.RangesCount, tmp.Rects); + IM_ASSERT(n == font_glyphs_count); + stbrp_pack_rects((stbrp_context*)spc.pack_info, tmp.Rects, n); + + // Extend texture height + for (int i = 0; i < n; i++) + if (tmp.Rects[i].was_packed) + atlas->TexHeight = ImMax(atlas->TexHeight, tmp.Rects[i].y + tmp.Rects[i].h); + } + IM_ASSERT(buf_rects_n == total_glyphs_count); + IM_ASSERT(buf_packedchars_n == total_glyphs_count); + IM_ASSERT(buf_ranges_n == total_ranges_count); + + // Create texture + atlas->TexHeight = (atlas->Flags & ImFontAtlasFlags_NoPowerOfTwoHeight) ? (atlas->TexHeight + 1) : ImUpperPowerOfTwo(atlas->TexHeight); + atlas->TexUvScale = ImVec2(1.0f / atlas->TexWidth, 1.0f / atlas->TexHeight); + atlas->TexPixelsAlpha8 = (unsigned char*)ImGui::MemAlloc(atlas->TexWidth * atlas->TexHeight); + memset(atlas->TexPixelsAlpha8, 0, atlas->TexWidth * atlas->TexHeight); + spc.pixels = atlas->TexPixelsAlpha8; + spc.height = atlas->TexHeight; + + // Second pass: render font characters + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + ImFontTempBuildData& tmp = tmp_array[input_i]; + stbtt_PackSetOversampling(&spc, cfg.OversampleH, cfg.OversampleV); + stbtt_PackFontRangesRenderIntoRects(&spc, &tmp.FontInfo, tmp.Ranges, tmp.RangesCount, tmp.Rects); + if (cfg.RasterizerMultiply != 1.0f) + { + unsigned char multiply_table[256]; + ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, cfg.RasterizerMultiply); + for (const stbrp_rect* r = tmp.Rects; r != tmp.Rects + tmp.RectsCount; r++) + if (r->was_packed) + ImFontAtlasBuildMultiplyRectAlpha8(multiply_table, spc.pixels, r->x, r->y, r->w, r->h, spc.stride_in_bytes); + } + tmp.Rects = NULL; + } + + // End packing + stbtt_PackEnd(&spc); + ImGui::MemFree(buf_rects); + buf_rects = NULL; + + // Third pass: setup ImFont and glyphs for runtime + for (int input_i = 0; input_i < atlas->ConfigData.Size; input_i++) + { + ImFontConfig& cfg = atlas->ConfigData[input_i]; + ImFontTempBuildData& tmp = tmp_array[input_i]; + ImFont* dst_font = cfg.DstFont; // We can have multiple input fonts writing into a same destination font (when using MergeMode=true) + + const float font_scale = stbtt_ScaleForPixelHeight(&tmp.FontInfo, cfg.SizePixels); + int unscaled_ascent, unscaled_descent, unscaled_line_gap; + stbtt_GetFontVMetrics(&tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); + + const float ascent = unscaled_ascent * font_scale; + const float descent = unscaled_descent * font_scale; + ImFontAtlasBuildSetupFont(atlas, dst_font, &cfg, ascent, descent); + const float off_x = cfg.GlyphOffset.x; + const float off_y = cfg.GlyphOffset.y + (float)(int)(dst_font->Ascent + 0.5f); + + for (int i = 0; i < tmp.RangesCount; i++) + { + stbtt_pack_range& range = tmp.Ranges[i]; + for (int char_idx = 0; char_idx < range.num_chars; char_idx += 1) + { + const stbtt_packedchar& pc = range.chardata_for_range[char_idx]; + if (!pc.x0 && !pc.x1 && !pc.y0 && !pc.y1) + continue; + + const int codepoint = range.first_unicode_codepoint_in_range + char_idx; + if (cfg.MergeMode && dst_font->FindGlyph((unsigned short)codepoint)) + continue; + + stbtt_aligned_quad q; + float dummy_x = 0.0f, dummy_y = 0.0f; + stbtt_GetPackedQuad(range.chardata_for_range, atlas->TexWidth, atlas->TexHeight, char_idx, &dummy_x, &dummy_y, &q, 0); + dst_font->AddGlyph((ImWchar)codepoint, q.x0 + off_x, q.y0 + off_y, q.x1 + off_x, q.y1 + off_y, q.s0, q.t0, q.s1, q.t1, pc.xadvance); + } + } + } + + // Cleanup temporaries + ImGui::MemFree(buf_packedchars); + ImGui::MemFree(buf_ranges); + ImGui::MemFree(tmp_array); + + ImFontAtlasBuildFinish(atlas); + + return true; +} + +void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas* atlas) +{ + if (atlas->CustomRectIds[0] >= 0) + return; + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + atlas->CustomRectIds[0] = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_ID, FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF*2+1, FONT_ATLAS_DEFAULT_TEX_DATA_H); + else + atlas->CustomRectIds[0] = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_ID, 2, 2); +} + +void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent) +{ + if (!font_config->MergeMode) + { + font->ClearOutputData(); + font->FontSize = font_config->SizePixels; + font->ConfigData = font_config; + font->ContainerAtlas = atlas; + font->Ascent = ascent; + font->Descent = descent; + } + font->ConfigDataCount++; +} + +void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* pack_context_opaque) +{ + stbrp_context* pack_context = (stbrp_context*)pack_context_opaque; + + ImVector& user_rects = atlas->CustomRects; + IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong. + + ImVector pack_rects; + pack_rects.resize(user_rects.Size); + memset(pack_rects.Data, 0, sizeof(stbrp_rect) * user_rects.Size); + for (int i = 0; i < user_rects.Size; i++) + { + pack_rects[i].w = user_rects[i].Width; + pack_rects[i].h = user_rects[i].Height; + } + stbrp_pack_rects(pack_context, &pack_rects[0], pack_rects.Size); + for (int i = 0; i < pack_rects.Size; i++) + if (pack_rects[i].was_packed) + { + user_rects[i].X = pack_rects[i].x; + user_rects[i].Y = pack_rects[i].y; + IM_ASSERT(pack_rects[i].w == user_rects[i].Width && pack_rects[i].h == user_rects[i].Height); + atlas->TexHeight = ImMax(atlas->TexHeight, pack_rects[i].y + pack_rects[i].h); + } +} + +static void ImFontAtlasBuildRenderDefaultTexData(ImFontAtlas* atlas) +{ + IM_ASSERT(atlas->CustomRectIds[0] >= 0); + IM_ASSERT(atlas->TexPixelsAlpha8 != NULL); + ImFontAtlas::CustomRect& r = atlas->CustomRects[atlas->CustomRectIds[0]]; + IM_ASSERT(r.ID == FONT_ATLAS_DEFAULT_TEX_DATA_ID); + IM_ASSERT(r.IsPacked()); + + const int w = atlas->TexWidth; + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + { + // Render/copy pixels + IM_ASSERT(r.Width == FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF * 2 + 1 && r.Height == FONT_ATLAS_DEFAULT_TEX_DATA_H); + for (int y = 0, n = 0; y < FONT_ATLAS_DEFAULT_TEX_DATA_H; y++) + for (int x = 0; x < FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF; x++, n++) + { + const int offset0 = (int)(r.X + x) + (int)(r.Y + y) * w; + const int offset1 = offset0 + FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF + 1; + atlas->TexPixelsAlpha8[offset0] = FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[n] == '.' ? 0xFF : 0x00; + atlas->TexPixelsAlpha8[offset1] = FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[n] == 'X' ? 0xFF : 0x00; + } + } + else + { + IM_ASSERT(r.Width == 2 && r.Height == 2); + const int offset = (int)(r.X) + (int)(r.Y) * w; + atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF; + } + atlas->TexUvWhitePixel = ImVec2((r.X + 0.5f) * atlas->TexUvScale.x, (r.Y + 0.5f) * atlas->TexUvScale.y); +} + +void ImFontAtlasBuildFinish(ImFontAtlas* atlas) +{ + // Render into our custom data block + ImFontAtlasBuildRenderDefaultTexData(atlas); + + // Register custom rectangle glyphs + for (int i = 0; i < atlas->CustomRects.Size; i++) + { + const ImFontAtlas::CustomRect& r = atlas->CustomRects[i]; + if (r.Font == NULL || r.ID > 0x10000) + continue; + + IM_ASSERT(r.Font->ContainerAtlas == atlas); + ImVec2 uv0, uv1; + atlas->CalcCustomRectUV(&r, &uv0, &uv1); + r.Font->AddGlyph((ImWchar)r.ID, r.GlyphOffset.x, r.GlyphOffset.y, r.GlyphOffset.x + r.Width, r.GlyphOffset.y + r.Height, uv0.x, uv0.y, uv1.x, uv1.y, r.GlyphAdvanceX); + } + + // Build all fonts lookup tables + for (int i = 0; i < atlas->Fonts.Size; i++) + atlas->Fonts[i]->BuildLookupTable(); +} + +// Retrieve list of range (2 int per range, values are inclusive) +const ImWchar* ImFontAtlas::GetGlyphRangesDefault() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesKorean() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3131, 0x3163, // Korean alphabets + 0xAC00, 0xD79D, // Korean characters + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesChinese() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3000, 0x30FF, // Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + 0x4e00, 0x9FAF, // CJK Ideograms + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesJapanese() +{ + // Store the 1946 ideograms code points as successive offsets from the initial unicode codepoint 0x4E00. Each offset has an implicit +1. + // This encoding is designed to helps us reduce the source code size. + // FIXME: Source a list of the revised 2136 joyo kanji list from 2010 and rebuild this. + // The current list was sourced from http://theinstructionlimit.com/author/renaudbedardrenaudbedard/page/3 + // Note that you may use ImFontAtlas::GlyphRangesBuilder to create your own ranges, by merging existing ranges or adding new characters. + static const short offsets_from_0x4E00[] = + { + -1,0,1,3,0,0,0,0,1,0,5,1,1,0,7,4,6,10,0,1,9,9,7,1,3,19,1,10,7,1,0,1,0,5,1,0,6,4,2,6,0,0,12,6,8,0,3,5,0,1,0,9,0,0,8,1,1,3,4,5,13,0,0,8,2,17, + 4,3,1,1,9,6,0,0,0,2,1,3,2,22,1,9,11,1,13,1,3,12,0,5,9,2,0,6,12,5,3,12,4,1,2,16,1,1,4,6,5,3,0,6,13,15,5,12,8,14,0,0,6,15,3,6,0,18,8,1,6,14,1, + 5,4,12,24,3,13,12,10,24,0,0,0,1,0,1,1,2,9,10,2,2,0,0,3,3,1,0,3,8,0,3,2,4,4,1,6,11,10,14,6,15,3,4,15,1,0,0,5,2,2,0,0,1,6,5,5,6,0,3,6,5,0,0,1,0, + 11,2,2,8,4,7,0,10,0,1,2,17,19,3,0,2,5,0,6,2,4,4,6,1,1,11,2,0,3,1,2,1,2,10,7,6,3,16,0,8,24,0,0,3,1,1,3,0,1,6,0,0,0,2,0,1,5,15,0,1,0,0,2,11,19, + 1,4,19,7,6,5,1,0,0,0,0,5,1,0,1,9,0,0,5,0,2,0,1,0,3,0,11,3,0,2,0,0,0,0,0,9,3,6,4,12,0,14,0,0,29,10,8,0,14,37,13,0,31,16,19,0,8,30,1,20,8,3,48, + 21,1,0,12,0,10,44,34,42,54,11,18,82,0,2,1,2,12,1,0,6,2,17,2,12,7,0,7,17,4,2,6,24,23,8,23,39,2,16,23,1,0,5,1,2,15,14,5,6,2,11,0,8,6,2,2,2,14, + 20,4,15,3,4,11,10,10,2,5,2,1,30,2,1,0,0,22,5,5,0,3,1,5,4,1,0,0,2,2,21,1,5,1,2,16,2,1,3,4,0,8,4,0,0,5,14,11,2,16,1,13,1,7,0,22,15,3,1,22,7,14, + 22,19,11,24,18,46,10,20,64,45,3,2,0,4,5,0,1,4,25,1,0,0,2,10,0,0,0,1,0,1,2,0,0,9,1,2,0,0,0,2,5,2,1,1,5,5,8,1,1,1,5,1,4,9,1,3,0,1,0,1,1,2,0,0, + 2,0,1,8,22,8,1,0,0,0,0,4,2,1,0,9,8,5,0,9,1,30,24,2,6,4,39,0,14,5,16,6,26,179,0,2,1,1,0,0,0,5,2,9,6,0,2,5,16,7,5,1,1,0,2,4,4,7,15,13,14,0,0, + 3,0,1,0,0,0,2,1,6,4,5,1,4,9,0,3,1,8,0,0,10,5,0,43,0,2,6,8,4,0,2,0,0,9,6,0,9,3,1,6,20,14,6,1,4,0,7,2,3,0,2,0,5,0,3,1,0,3,9,7,0,3,4,0,4,9,1,6,0, + 9,0,0,2,3,10,9,28,3,6,2,4,1,2,32,4,1,18,2,0,3,1,5,30,10,0,2,2,2,0,7,9,8,11,10,11,7,2,13,7,5,10,0,3,40,2,0,1,6,12,0,4,5,1,5,11,11,21,4,8,3,7, + 8,8,33,5,23,0,0,19,8,8,2,3,0,6,1,1,1,5,1,27,4,2,5,0,3,5,6,3,1,0,3,1,12,5,3,3,2,0,7,7,2,1,0,4,0,1,1,2,0,10,10,6,2,5,9,7,5,15,15,21,6,11,5,20, + 4,3,5,5,2,5,0,2,1,0,1,7,28,0,9,0,5,12,5,5,18,30,0,12,3,3,21,16,25,32,9,3,14,11,24,5,66,9,1,2,0,5,9,1,5,1,8,0,8,3,3,0,1,15,1,4,8,1,2,7,0,7,2, + 8,3,7,5,3,7,10,2,1,0,0,2,25,0,6,4,0,10,0,4,2,4,1,12,5,38,4,0,4,1,10,5,9,4,0,14,4,2,5,18,20,21,1,3,0,5,0,7,0,3,7,1,3,1,1,8,1,0,0,0,3,2,5,2,11, + 6,0,13,1,3,9,1,12,0,16,6,2,1,0,2,1,12,6,13,11,2,0,28,1,7,8,14,13,8,13,0,2,0,5,4,8,10,2,37,42,19,6,6,7,4,14,11,18,14,80,7,6,0,4,72,12,36,27, + 7,7,0,14,17,19,164,27,0,5,10,7,3,13,6,14,0,2,2,5,3,0,6,13,0,0,10,29,0,4,0,3,13,0,3,1,6,51,1,5,28,2,0,8,0,20,2,4,0,25,2,10,13,10,0,16,4,0,1,0, + 2,1,7,0,1,8,11,0,0,1,2,7,2,23,11,6,6,4,16,2,2,2,0,22,9,3,3,5,2,0,15,16,21,2,9,20,15,15,5,3,9,1,0,0,1,7,7,5,4,2,2,2,38,24,14,0,0,15,5,6,24,14, + 5,5,11,0,21,12,0,3,8,4,11,1,8,0,11,27,7,2,4,9,21,59,0,1,39,3,60,62,3,0,12,11,0,3,30,11,0,13,88,4,15,5,28,13,1,4,48,17,17,4,28,32,46,0,16,0, + 18,11,1,8,6,38,11,2,6,11,38,2,0,45,3,11,2,7,8,4,30,14,17,2,1,1,65,18,12,16,4,2,45,123,12,56,33,1,4,3,4,7,0,0,0,3,2,0,16,4,2,4,2,0,7,4,5,2,26, + 2,25,6,11,6,1,16,2,6,17,77,15,3,35,0,1,0,5,1,0,38,16,6,3,12,3,3,3,0,9,3,1,3,5,2,9,0,18,0,25,1,3,32,1,72,46,6,2,7,1,3,14,17,0,28,1,40,13,0,20, + 15,40,6,38,24,12,43,1,1,9,0,12,6,0,6,2,4,19,3,7,1,48,0,9,5,0,5,6,9,6,10,15,2,11,19,3,9,2,0,1,10,1,27,8,1,3,6,1,14,0,26,0,27,16,3,4,9,6,2,23, + 9,10,5,25,2,1,6,1,1,48,15,9,15,14,3,4,26,60,29,13,37,21,1,6,4,0,2,11,22,23,16,16,2,2,1,3,0,5,1,6,4,0,0,4,0,0,8,3,0,2,5,0,7,1,7,3,13,2,4,10, + 3,0,2,31,0,18,3,0,12,10,4,1,0,7,5,7,0,5,4,12,2,22,10,4,2,15,2,8,9,0,23,2,197,51,3,1,1,4,13,4,3,21,4,19,3,10,5,40,0,4,1,1,10,4,1,27,34,7,21, + 2,17,2,9,6,4,2,3,0,4,2,7,8,2,5,1,15,21,3,4,4,2,2,17,22,1,5,22,4,26,7,0,32,1,11,42,15,4,1,2,5,0,19,3,1,8,6,0,10,1,9,2,13,30,8,2,24,17,19,1,4, + 4,25,13,0,10,16,11,39,18,8,5,30,82,1,6,8,18,77,11,13,20,75,11,112,78,33,3,0,0,60,17,84,9,1,1,12,30,10,49,5,32,158,178,5,5,6,3,3,1,3,1,4,7,6, + 19,31,21,0,2,9,5,6,27,4,9,8,1,76,18,12,1,4,0,3,3,6,3,12,2,8,30,16,2,25,1,5,5,4,3,0,6,10,2,3,1,0,5,1,19,3,0,8,1,5,2,6,0,0,0,19,1,2,0,5,1,2,5, + 1,3,7,0,4,12,7,3,10,22,0,9,5,1,0,2,20,1,1,3,23,30,3,9,9,1,4,191,14,3,15,6,8,50,0,1,0,0,4,0,0,1,0,2,4,2,0,2,3,0,2,0,2,2,8,7,0,1,1,1,3,3,17,11, + 91,1,9,3,2,13,4,24,15,41,3,13,3,1,20,4,125,29,30,1,0,4,12,2,21,4,5,5,19,11,0,13,11,86,2,18,0,7,1,8,8,2,2,22,1,2,6,5,2,0,1,2,8,0,2,0,5,2,1,0, + 2,10,2,0,5,9,2,1,2,0,1,0,4,0,0,10,2,5,3,0,6,1,0,1,4,4,33,3,13,17,3,18,6,4,7,1,5,78,0,4,1,13,7,1,8,1,0,35,27,15,3,0,0,0,1,11,5,41,38,15,22,6, + 14,14,2,1,11,6,20,63,5,8,27,7,11,2,2,40,58,23,50,54,56,293,8,8,1,5,1,14,0,1,12,37,89,8,8,8,2,10,6,0,0,0,4,5,2,1,0,1,1,2,7,0,3,3,0,4,6,0,3,2, + 19,3,8,0,0,0,4,4,16,0,4,1,5,1,3,0,3,4,6,2,17,10,10,31,6,4,3,6,10,126,7,3,2,2,0,9,0,0,5,20,13,0,15,0,6,0,2,5,8,64,50,3,2,12,2,9,0,0,11,8,20, + 109,2,18,23,0,0,9,61,3,0,28,41,77,27,19,17,81,5,2,14,5,83,57,252,14,154,263,14,20,8,13,6,57,39,38, + }; + static ImWchar base_ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x3000, 0x30FF, // Punctuations, Hiragana, Katakana + 0x31F0, 0x31FF, // Katakana Phonetic Extensions + 0xFF00, 0xFFEF, // Half-width characters + }; + static bool full_ranges_unpacked = false; + static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(offsets_from_0x4E00)*2 + 1]; + if (!full_ranges_unpacked) + { + // Unpack + int codepoint = 0x4e00; + memcpy(full_ranges, base_ranges, sizeof(base_ranges)); + ImWchar* dst = full_ranges + IM_ARRAYSIZE(base_ranges);; + for (int n = 0; n < IM_ARRAYSIZE(offsets_from_0x4E00); n++, dst += 2) + dst[0] = dst[1] = (ImWchar)(codepoint += (offsets_from_0x4E00[n] + 1)); + dst[0] = 0; + full_ranges_unpacked = true; + } + return &full_ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesCyrillic() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x0400, 0x052F, // Cyrillic + Cyrillic Supplement + 0x2DE0, 0x2DFF, // Cyrillic Extended-A + 0xA640, 0xA69F, // Cyrillic Extended-B + 0, + }; + return &ranges[0]; +} + +const ImWchar* ImFontAtlas::GetGlyphRangesThai() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + 0x2010, 0x205E, // Punctuations + 0x0E00, 0x0E7F, // Thai + 0, + }; + return &ranges[0]; +} + +//----------------------------------------------------------------------------- +// ImFontAtlas::GlyphRangesBuilder +//----------------------------------------------------------------------------- + +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); + } +} + +void ImFontAtlas::GlyphRangesBuilder::AddRanges(const ImWchar* ranges) +{ + for (; ranges[0]; ranges += 2) + for (ImWchar c = ranges[0]; c <= ranges[1]; c++) + AddChar(c); +} + +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); +} + +//----------------------------------------------------------------------------- +// ImFont +//----------------------------------------------------------------------------- + +ImFont::ImFont() +{ + 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(); +} + +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; +} + +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; +} + +void ImFont::SetFallbackChar(ImWchar c) +{ + 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); +} + +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); +} + +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; + + if (dst < index_size && IndexLookup.Data[dst] == (unsigned short)-1 && !overwrite_dst) // 'dst' already exists + return; + if (src >= index_size && dst >= index_size) // both 'dst' and 'src' don't exist -> no-op + return; + + GrowIndex(dst + 1); + IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (unsigned short)-1; + IndexAdvanceX[dst] = (src < index_size) ? IndexAdvanceX.Data[src] : 1.0f; +} + +const ImFontGlyph* ImFont::FindGlyph(ImWchar c) const +{ + if (c < IndexLookup.Size) + { + const unsigned short i = IndexLookup[c]; + if (i != (unsigned short)-1) + return &Glyphs.Data[i]; + } + return FallbackGlyph; +} + +const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const +{ + // Simple word-wrapping for English, not full-featured. Please submit failing cases! + // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) + + // For references, possible wrap point marked with ^ + // "aaa bbb, ccc,ddd. eee fff. ggg!" + // ^ ^ ^ ^ ^__ ^ ^ + + // List of hardcoded separators: .,;!?'" + + // Skip extra blanks after a line returns (that includes not counting them in width computation) + // e.g. "Hello world" --> "Hello" "World" + + // Cut words that cannot possibly fit within one line. + // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" + + float line_width = 0.0f; + float word_width = 0.0f; + float blank_width = 0.0f; + wrap_width /= scale; // We work with unscaled widths to avoid scaling every characters + + const char* word_end = text; + const char* prev_word_end = NULL; + bool inside_word = true; + + const char* s = text; + while (s < text_end) + { + unsigned int c = (unsigned int)*s; + const char* next_s; + if (c < 0x80) + next_s = s + 1; + else + next_s = s + ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) + break; + + if (c < 32) + { + if (c == '\n') + { + line_width = word_width = blank_width = 0.0f; + inside_word = true; + s = next_s; + continue; + } + if (c == '\r') + { + s = next_s; + continue; + } + } + + const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX[(int)c] : FallbackAdvanceX); + if (ImCharIsSpace(c)) + { + if (inside_word) + { + line_width += blank_width; + blank_width = 0.0f; + word_end = s; + } + blank_width += char_width; + inside_word = false; + } + else + { + word_width += char_width; + if (inside_word) + { + word_end = next_s; + } + else + { + prev_word_end = word_end; + line_width += word_width + blank_width; + word_width = blank_width = 0.0f; + } + + // Allow wrapping after punctuation. + inside_word = !(c == '.' || c == ',' || c == ';' || c == '!' || c == '?' || c == '\"'); + } + + // We ignore blank width at the end of the line (they can be skipped) + if (line_width + word_width >= wrap_width) + { + // Words that cannot possibly fit within an entire line will be cut anywhere. + if (word_width < wrap_width) + s = prev_word_end ? prev_word_end : word_end; + break; + } + + s = next_s; + } + + return s; +} + +ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) const +{ + if (!text_end) + text_end = text_begin + strlen(text_begin); // FIXME-OPT: Need to avoid this. + + const float line_height = size; + const float scale = size / FontSize; + + ImVec2 text_size = ImVec2(0,0); + float line_width = 0.0f; + + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + + const char* s = text_begin; + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + { + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width); + if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below + } + + if (s >= word_wrap_eol) + { + if (text_size.x < line_width) + text_size.x = line_width; + text_size.y += line_height; + line_width = 0.0f; + word_wrap_eol = NULL; + + // Wrapping skips upcoming blanks + while (s < text_end) + { + const char c = *s; + if (ImCharIsSpace(c)) { s++; } else if (c == '\n') { s++; break; } else { break; } + } + continue; + } + } + + // Decode and advance source + const char* prev_s = s; + unsigned int c = (unsigned int)*s; + if (c < 0x80) + { + s += 1; + } + else + { + s += ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) // Malformed UTF-8? + break; + } + + if (c < 32) + { + if (c == '\n') + { + text_size.x = ImMax(text_size.x, line_width); + text_size.y += line_height; + line_width = 0.0f; + continue; + } + if (c == '\r') + continue; + } + + const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX[(int)c] : FallbackAdvanceX) * scale; + if (line_width + char_width >= max_width) + { + s = prev_s; + break; + } + + line_width += char_width; + } + + if (text_size.x < line_width) + text_size.x = line_width; + + if (line_width > 0 || text_size.y == 0.0f) + text_size.y += line_height; + + if (remaining) + *remaining = s; + + return text_size; +} + +void ImFont::RenderChar(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, unsigned short c) const +{ + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') // Match behavior of RenderText(), those 4 codepoints are hard-coded. + return; + if (const ImFontGlyph* glyph = FindGlyph(c)) + { + float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; + pos.x = (float)(int)pos.x + DisplayOffset.x; + pos.y = (float)(int)pos.y + DisplayOffset.y; + draw_list->PrimReserve(6, 4); + draw_list->PrimRectUV(ImVec2(pos.x + glyph->X0 * scale, pos.y + glyph->Y0 * scale), ImVec2(pos.x + glyph->X1 * scale, pos.y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); + } +} + +void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) const +{ + if (!text_end) + text_end = text_begin + strlen(text_begin); // ImGui functions generally already provides a valid text_end, so this is merely to handle direct calls. + + // Align to be pixel perfect + pos.x = (float)(int)pos.x + DisplayOffset.x; + pos.y = (float)(int)pos.y + DisplayOffset.y; + float x = pos.x; + float y = pos.y; + if (y > clip_rect.w) + return; + + const float scale = size / FontSize; + const float line_height = FontSize * scale; + const bool word_wrap_enabled = (wrap_width > 0.0f); + const char* word_wrap_eol = NULL; + + // Skip non-visible lines + const char* s = text_begin; + if (!word_wrap_enabled && y + line_height < clip_rect.y) + while (s < text_end && *s != '\n') // Fast-forward to next line + s++; + + // Reserve vertices for remaining worse case (over-reserving is useful and easily amortized) + const int vtx_count_max = (int)(text_end - s) * 4; + const int idx_count_max = (int)(text_end - s) * 6; + const int idx_expected_size = draw_list->IdxBuffer.Size + idx_count_max; + draw_list->PrimReserve(idx_count_max, vtx_count_max); + + ImDrawVert* vtx_write = draw_list->_VtxWritePtr; + ImDrawIdx* idx_write = draw_list->_IdxWritePtr; + unsigned int vtx_current_idx = draw_list->_VtxCurrentIdx; + + while (s < text_end) + { + if (word_wrap_enabled) + { + // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. + if (!word_wrap_eol) + { + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - pos.x)); + if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below + } + + if (s >= word_wrap_eol) + { + x = pos.x; + y += line_height; + word_wrap_eol = NULL; + + // Wrapping skips upcoming blanks + while (s < text_end) + { + const char c = *s; + if (ImCharIsSpace(c)) { s++; } else if (c == '\n') { s++; break; } else { break; } + } + continue; + } + } + + // Decode and advance source + unsigned int c = (unsigned int)*s; + if (c < 0x80) + { + s += 1; + } + else + { + s += ImTextCharFromUtf8(&c, s, text_end); + if (c == 0) // Malformed UTF-8? + break; + } + + if (c < 32) + { + if (c == '\n') + { + x = pos.x; + y += line_height; + + if (y > clip_rect.w) + break; + if (!word_wrap_enabled && y + line_height < clip_rect.y) + while (s < text_end && *s != '\n') // Fast-forward to next line + s++; + continue; + } + if (c == '\r') + continue; + } + + float char_width = 0.0f; + if (const ImFontGlyph* glyph = FindGlyph((unsigned short)c)) + { + char_width = glyph->AdvanceX * scale; + + // Arbitrarily assume that both space and tabs are empty glyphs as an optimization + if (c != ' ' && c != '\t') + { + // We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w + float x1 = x + glyph->X0 * scale; + float x2 = x + glyph->X1 * scale; + float y1 = y + glyph->Y0 * scale; + float y2 = y + glyph->Y1 * scale; + if (x1 <= clip_rect.z && x2 >= clip_rect.x) + { + // Render a character + float u1 = glyph->U0; + float v1 = glyph->V0; + float u2 = glyph->U1; + float v2 = glyph->V1; + + // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. + if (cpu_fine_clip) + { + if (x1 < clip_rect.x) + { + u1 = u1 + (1.0f - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1); + x1 = clip_rect.x; + } + if (y1 < clip_rect.y) + { + v1 = v1 + (1.0f - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1); + y1 = clip_rect.y; + } + if (x2 > clip_rect.z) + { + u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1); + x2 = clip_rect.z; + } + if (y2 > clip_rect.w) + { + v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1); + y2 = clip_rect.w; + } + if (y1 >= y2) + { + x += char_width; + continue; + } + } + + // We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: + { + idx_write[0] = (ImDrawIdx)(vtx_current_idx); idx_write[1] = (ImDrawIdx)(vtx_current_idx+1); idx_write[2] = (ImDrawIdx)(vtx_current_idx+2); + idx_write[3] = (ImDrawIdx)(vtx_current_idx); idx_write[4] = (ImDrawIdx)(vtx_current_idx+2); idx_write[5] = (ImDrawIdx)(vtx_current_idx+3); + vtx_write[0].pos.x = x1; vtx_write[0].pos.y = y1; vtx_write[0].col = col; vtx_write[0].uv.x = u1; vtx_write[0].uv.y = v1; + vtx_write[1].pos.x = x2; vtx_write[1].pos.y = y1; vtx_write[1].col = col; vtx_write[1].uv.x = u2; vtx_write[1].uv.y = v1; + vtx_write[2].pos.x = x2; vtx_write[2].pos.y = y2; vtx_write[2].col = col; vtx_write[2].uv.x = u2; vtx_write[2].uv.y = v2; + vtx_write[3].pos.x = x1; vtx_write[3].pos.y = y2; vtx_write[3].col = col; vtx_write[3].uv.x = u1; vtx_write[3].uv.y = v2; + vtx_write += 4; + vtx_current_idx += 4; + idx_write += 6; + } + } + } + } + + x += char_width; + } + + // Give back unused vertices + draw_list->VtxBuffer.resize((int)(vtx_write - draw_list->VtxBuffer.Data)); + draw_list->IdxBuffer.resize((int)(idx_write - draw_list->IdxBuffer.Data)); + draw_list->CmdBuffer[draw_list->CmdBuffer.Size-1].ElemCount -= (idx_expected_size - draw_list->IdxBuffer.Size); + draw_list->_VtxWritePtr = vtx_write; + draw_list->_IdxWritePtr = idx_write; + draw_list->_VtxCurrentIdx = (unsigned int)draw_list->VtxBuffer.Size; +} + +//----------------------------------------------------------------------------- +// Internals Drawing Helpers +//----------------------------------------------------------------------------- + +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. +} + +// FIXME: Cleanup and move code to ImDrawList. +void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding) +{ + if (x_end_norm == x_start_norm) + return; + if (x_start_norm > x_end_norm) + ImSwap(x_start_norm, x_end_norm); + + ImVec2 p0 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_start_norm), rect.Min.y); + ImVec2 p1 = ImVec2(ImLerp(rect.Min.x, rect.Max.x, x_end_norm), rect.Max.y); + if (rounding == 0.0f) + { + draw_list->AddRectFilled(p0, p1, col, 0.0f); + return; + } + + rounding = ImClamp(ImMin((rect.Max.x - rect.Min.x) * 0.5f, (rect.Max.y - rect.Min.y) * 0.5f) - 1.0f, 0.0f, rounding); + const float inv_rounding = 1.0f / rounding; + const float arc0_b = ImAcos01(1.0f - (p0.x - rect.Min.x) * inv_rounding); + const float arc0_e = ImAcos01(1.0f - (p1.x - rect.Min.x) * inv_rounding); + const float x0 = ImMax(p0.x, rect.Min.x + rounding); + if (arc0_b == arc0_e) + { + draw_list->PathLineTo(ImVec2(x0, p1.y)); + draw_list->PathLineTo(ImVec2(x0, p0.y)); + } + else if (arc0_b == 0.0f && arc0_e == IM_PI*0.5f) + { + draw_list->PathArcToFast(ImVec2(x0, p1.y - rounding), rounding, 3, 6); // BL + draw_list->PathArcToFast(ImVec2(x0, p0.y + rounding), rounding, 6, 9); // TR + } + else + { + draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b, 3); // BL + draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e, 3); // TR + } + if (p1.x > rect.Min.x + rounding) + { + const float arc1_b = ImAcos01(1.0f - (rect.Max.x - p1.x) * inv_rounding); + const float arc1_e = ImAcos01(1.0f - (rect.Max.x - p0.x) * inv_rounding); + const float x1 = ImMin(p1.x, rect.Max.x - rounding); + if (arc1_b == arc1_e) + { + draw_list->PathLineTo(ImVec2(x1, p0.y)); + draw_list->PathLineTo(ImVec2(x1, p1.y)); + } + else if (arc1_b == 0.0f && arc1_e == IM_PI*0.5f) + { + draw_list->PathArcToFast(ImVec2(x1, p0.y + rounding), rounding, 9, 12); // TR + draw_list->PathArcToFast(ImVec2(x1, p1.y - rounding), rounding, 0, 3); // BR + } + else + { + draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b, 3); // TR + draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e, 3); // BR + } + } + draw_list->PathFillConvex(col); +} + +//----------------------------------------------------------------------------- +// DEFAULT FONT DATA +//----------------------------------------------------------------------------- +// Compressed with stb_compress() then converted to a C array. +// Use the program in misc/fonts/binary_to_compressed_c.cpp to create the array from a TTF file. +// Decompression from stb.h (public domain) by Sean Barrett https://github.com/nothings/stb/blob/master/stb.h +//----------------------------------------------------------------------------- + +static unsigned int stb_decompress_length(unsigned char *input) +{ + 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) +{ + // INVERSE of memmove... write each byte before copying the next... + IM_ASSERT (stb__dout + length <= stb__barrier); + if (stb__dout + length > stb__barrier) { stb__dout += length; return; } + if (data < stb__barrier4) { stb__dout = stb__barrier+1; return; } + while (length--) *stb__dout++ = *data++; +} + +static void stb__lit(unsigned char *data, unsigned int length) +{ + IM_ASSERT (stb__dout + length <= stb__barrier); + if (stb__dout + length > stb__barrier) { stb__dout += length; return; } + if (data < stb__barrier2) { stb__dout = stb__barrier+1; return; } + memcpy(stb__dout, data, length); + stb__dout += length; +} + +#define stb__in2(x) ((i[x] << 8) + i[(x)+1]) +#define stb__in3(x) ((i[x] << 16) + stb__in2((x)+1)) +#define stb__in4(x) ((i[x] << 24) + stb__in3((x)+1)) + +static unsigned char *stb_decompress_token(unsigned char *i) +{ + if (*i >= 0x20) { // use fewer if's for cases that expand small + if (*i >= 0x80) stb__match(stb__dout-i[1]-1, i[0] - 0x80 + 1), i += 2; + else if (*i >= 0x40) stb__match(stb__dout-(stb__in2(0) - 0x4000 + 1), i[2]+1), i += 3; + else /* *i >= 0x20 */ stb__lit(i+1, i[0] - 0x20 + 1), i += 1 + (i[0] - 0x20 + 1); + } else { // more ifs for cases that expand large, since overhead is amortized + if (*i >= 0x18) stb__match(stb__dout-(stb__in3(0) - 0x180000 + 1), i[3]+1), i += 4; + else if (*i >= 0x10) stb__match(stb__dout-(stb__in3(0) - 0x100000 + 1), stb__in2(3)+1), i += 5; + else if (*i >= 0x08) stb__lit(i+2, stb__in2(0) - 0x0800 + 1), i += 2 + (stb__in2(0) - 0x0800 + 1); + else if (*i == 0x07) stb__lit(i+3, stb__in2(1) + 1), i += 3 + (stb__in2(1) + 1); + else if (*i == 0x06) stb__match(stb__dout-(stb__in3(1)+1), i[4]+1), i += 5; + else if (*i == 0x04) stb__match(stb__dout-(stb__in3(1)+1), stb__in2(4)+1), i += 6; + } + return i; +} + +static unsigned int stb_adler32(unsigned int adler32, unsigned char *buffer, unsigned int buflen) +{ + const unsigned long ADLER_MOD = 65521; + unsigned long s1 = adler32 & 0xffff, s2 = adler32 >> 16; + unsigned long blocklen, i; + + blocklen = buflen % 5552; + while (buflen) { + for (i=0; i + 7 < blocklen; i += 8) { + s1 += buffer[0], s2 += s1; + s1 += buffer[1], s2 += s1; + s1 += buffer[2], s2 += s1; + s1 += buffer[3], s2 += s1; + s1 += buffer[4], s2 += s1; + s1 += buffer[5], s2 += s1; + s1 += buffer[6], s2 += s1; + s1 += buffer[7], s2 += s1; + + buffer += 8; + } + + for (; i < blocklen; ++i) + s1 += *buffer++, s2 += s1; + + s1 %= ADLER_MOD, s2 %= ADLER_MOD; + buflen -= blocklen; + blocklen = 5552; + } + return (unsigned int)(s2 << 16) + (unsigned int)s1; +} + +static unsigned int stb_decompress(unsigned char *output, unsigned char *i, unsigned int length) +{ + unsigned int olen; + if (stb__in4(0) != 0x57bC0000) return 0; + if (stb__in4(4) != 0) return 0; // error! stream is > 4GB + olen = stb_decompress_length(i); + stb__barrier2 = i; + stb__barrier3 = i+length; + stb__barrier = output + olen; + stb__barrier4 = output; + i += 16; + + stb__dout = output; + for (;;) { + unsigned char *old_i = i; + i = stb_decompress_token(i); + if (i == old_i) { + if (*i == 0x05 && i[1] == 0xfa) { + IM_ASSERT(stb__dout == output + olen); + if (stb__dout != output + olen) return 0; + if (stb_adler32(1, output, olen) != (unsigned int) stb__in4(2)) + return 0; + return olen; + } else { + IM_ASSERT(0); /* NOTREACHED */ + return 0; + } + } + IM_ASSERT(stb__dout <= output + olen); + if (stb__dout > output + olen) + return 0; + } +} + +//----------------------------------------------------------------------------- +// ProggyClean.ttf +// Copyright (c) 2004, 2005 Tristan Grimmer +// MIT license (see License.txt in http://www.upperbounds.net/download/ProggyClean.ttf.zip) +// Download and more information at http://upperbounds.net +//----------------------------------------------------------------------------- +// File: 'ProggyClean.ttf' (41208 bytes) +// Exported using binary_to_compressed_c.cpp +//----------------------------------------------------------------------------- +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$FBjVQTSDgEKnIS7EM9>ZY9w0#L;>>#Mx&4Mvt//L[MkA#W@lK.N'[0#7RL_&#w+F%HtG9M#XL`N&.,GM4Pg;--VsM.M0rJfLH2eTM`*oJMHRC`N" + "kfimM2J,W-jXS:)r0wK#@Fge$U>`w'N7G#$#fB#$E^$#:9:hk+eOe--6x)F7*E%?76%^GMHePW-Z5l'&GiF#$956:rS?dA#fiK:)Yr+`�j@'DbG&#^$PG.Ll+DNa&VZ>1i%h1S9u5o@YaaW$e+bROPOpxTO7Stwi1::iB1q)C_=dV26J;2,]7op$]uQr@_V7$q^%lQwtuHY]=DX,n3L#0PHDO4f9>dC@O>HBuKPpP*E,N+b3L#lpR/MrTEH.IAQk.a>D[.e;mc." + "x]Ip.PH^'/aqUO/$1WxLoW0[iLAw=4h(9.`G" + "CRUxHPeR`5Mjol(dUWxZa(>STrPkrJiWx`5U7F#.g*jrohGg`cg:lSTvEY/EV_7H4Q9[Z%cnv;JQYZ5q.l7Zeas:HOIZOB?Ggv:[7MI2k).'2($5FNP&EQ(,)" + "U]W]+fh18.vsai00);D3@4ku5P?DP8aJt+;qUM]=+b'8@;mViBKx0DE[-auGl8:PJ&Dj+M6OC]O^((##]`0i)drT;-7X`=-H3[igUnPG-NZlo.#k@h#=Ork$m>a>$-?Tm$UV(?#P6YY#" + "'/###xe7q.73rI3*pP/$1>s9)W,JrM7SN]'/4C#v$U`0#V.[0>xQsH$fEmPMgY2u7Kh(G%siIfLSoS+MK2eTM$=5,M8p`A.;_R%#u[K#$x4AG8.kK/HSB==-'Ie/QTtG?-.*^N-4B/ZM" + "_3YlQC7(p7q)&](`6_c)$/*JL(L-^(]$wIM`dPtOdGA,U3:w2M-0+WomX2u7lqM2iEumMTcsF?-aT=Z-97UEnXglEn1K-bnEO`gu" + "Ft(c%=;Am_Qs@jLooI&NX;]0#j4#F14;gl8-GQpgwhrq8'=l_f-b49'UOqkLu7-##oDY2L(te+Mch&gLYtJ,MEtJfLh'x'M=$CS-ZZ%P]8bZ>#S?YY#%Q&q'3^Fw&?D)UDNrocM3A76/" + "/oL?#h7gl85[qW/NDOk%16ij;+:1a'iNIdb-ou8.P*w,v5#EI$TWS>Pot-R*H'-SEpA:g)f+O$%%`kA#G=8RMmG1&O`>to8bC]T&$,n.LoO>29sp3dt-52U%VM#q7'DHpg+#Z9%H[Ket`e;)f#Km8&+DC$I46>#Kr]]u-[=99tts1.qb#q72g1WJO81q+eN'03'eM>&1XxY-caEnO" + "j%2n8)),?ILR5^.Ibn<-X-Mq7[a82Lq:F&#ce+S9wsCK*x`569E8ew'He]h:sI[2LM$[guka3ZRd6:t%IG:;$%YiJ:Nq=?eAw;/:nnDq0(CYcMpG)qLN4$##&J-XTt,%OVU4)S1+R-#dg0/Nn?Ku1^0f$B*P:Rowwm-`0PKjYDDM'3]d39VZHEl4,.j']Pk-M.h^&:0FACm$maq-&sgw0t7/6(^xtk%" + "LuH88Fj-ekm>GA#_>568x6(OFRl-IZp`&b,_P'$MhLbxfc$mj`,O;&%W2m`Zh:/)Uetw:aJ%]K9h:TcF]u_-Sj9,VK3M.*'&0D[Ca]J9gp8,kAW]" + "%(?A%R$f<->Zts'^kn=-^@c4%-pY6qI%J%1IGxfLU9CP8cbPlXv);C=b),<2mOvP8up,UVf3839acAWAW-W?#ao/^#%KYo8fRULNd2.>%m]UK:n%r$'sw]J;5pAoO_#2mO3n,'=H5(et" + "Hg*`+RLgv>=4U8guD$I%D:W>-r5V*%j*W:Kvej.Lp$'?;++O'>()jLR-^u68PHm8ZFWe+ej8h:9r6L*0//c&iH&R8pRbA#Kjm%upV1g:" + "a_#Ur7FuA#(tRh#.Y5K+@?3<-8m0$PEn;J:rh6?I6uG<-`wMU'ircp0LaE_OtlMb&1#6T.#FDKu#1Lw%u%+GM+X'e?YLfjM[VO0MbuFp7;>Q&#WIo)0@F%q7c#4XAXN-U&VBpqB>0ie&jhZ[?iLR@@_AvA-iQC(=ksRZRVp7`.=+NpBC%rh&3]R:8XDmE5^V8O(x<-+k?'(^](H.aREZSi,#1:[IXaZFOm<-ui#qUq2$##Ri;u75OK#(RtaW-K-F`S+cF]uN`-KMQ%rP/Xri.LRcB##=YL3BgM/3M" + "D?@f&1'BW-)Ju#bmmWCMkk&#TR`C,5d>g)F;t,4:@_l8G/5h4vUd%&%950:VXD'QdWoY-F$BtUwmfe$YqL'8(PWX(" + "P?^@Po3$##`MSs?DWBZ/S>+4%>fX,VWv/w'KD`LP5IbH;rTV>n3cEK8U#bX]l-/V+^lj3;vlMb&[5YQ8#pekX9JP3XUC72L,,?+Ni&co7ApnO*5NK,((W-i:$,kp'UDAO(G0Sq7MVjJs" + "bIu)'Z,*[>br5fX^:FPAWr-m2KgLQ_nN6'8uTGT5g)uLv:873UpTLgH+#FgpH'_o1780Ph8KmxQJ8#H72L4@768@Tm&Q" + "h4CB/5OvmA&,Q&QbUoi$a_%3M01H)4x7I^&KQVgtFnV+;[Pc>[m4k//,]1?#`VY[Jr*3&&slRfLiVZJ:]?=K3Sw=[$=uRB?3xk48@aege0jT6'N#(q%.O=?2S]u*(m<-" + "V8J'(1)G][68hW$5'q[GC&5j`TE?m'esFGNRM)j,ffZ?-qx8;->g4t*:CIP/[Qap7/9'#(1sao7w-.qNUdkJ)tCF&#B^;xGvn2r9FEPFFFcL@.iFNkTve$m%#QvQS8U@)2Z+3K:AKM5i" + "sZ88+dKQ)W6>J%CL`.d*(B`-n8D9oK-XV1q['-5k'cAZ69e;D_?$ZPP&s^+7])$*$#@QYi9,5P r+$%CE=68>K8r0=dSC%%(@p7" + ".m7jilQ02'0-VWAgTlGW'b)Tq7VT9q^*^$$.:&N@@" + "$&)WHtPm*5_rO0&e%K&#-30j(E4#'Zb.o/(Tpm$>K'f@[PvFl,hfINTNU6u'0pao7%XUp9]5.>%h`8_=VYbxuel.NTSsJfLacFu3B'lQSu/m6-Oqem8T+oE--$0a/k]uj9EwsG>%veR*" + "hv^BFpQj:K'#SJ,sB-'#](j.Lg92rTw-*n%@/;39rrJF,l#qV%OrtBeC6/,;qB3ebNW[?,Hqj2L.1NP&GjUR=1D8QaS3Up&@*9wP?+lo7b?@%'k4`p0Z$22%K3+iCZj?XJN4Nm&+YF]u" + "@-W$U%VEQ/,,>>#)D#%8cY#YZ?=,`Wdxu/ae&#" + "w6)R89tI#6@s'(6Bf7a&?S=^ZI_kS&ai`&=tE72L_D,;^R)7[$so8lKN%5/$(vdfq7+ebA#" + "u1p]ovUKW&Y%q]'>$1@-[xfn$7ZTp7mM,G,Ko7a&Gu%G[RMxJs[0MM%wci.LFDK)(%:_i2B5CsR8&9Z&#=mPEnm0f`<&c)QL5uJ#%u%lJj+D-r;BoFDoS97h5g)E#o:&S4weDF,9^Hoe`h*L+_a*NrLW-1pG_&2UdB8" + "6e%B/:=>)N4xeW.*wft-;$'58-ESqr#U`'6AQ]m&6/`Z>#S?YY#Vc;r7U2&326d=w&H####?TZ`*4?&.MK?LP8Vxg>$[QXc%QJv92.(Db*B)gb*BM9dM*hJMAo*c&#" + "b0v=Pjer]$gG&JXDf->'StvU7505l9$AFvgYRI^&<^b68?j#q9QX4SM'RO#&sL1IM.rJfLUAj221]d##DW=m83u5;'bYx,*Sl0hL(W;;$doB&O/TQ:(Z^xBdLjLV#*8U_72Lh+2Q8Cj0i:6hp&$C/:p(HK>T8Y[gHQ4`4)'$Ab(Nof%V'8hL&#SfD07&6D@M.*J:;$-rv29'M]8qMv-tLp,'886iaC=Hb*YJoKJ,(j%K=H`K.v9HggqBIiZu'QvBT.#=)0ukruV&.)3=(^1`o*Pj4<-#MJ+gLq9-##@HuZPN0]u:h7.T..G:;$/Usj(T7`Q8tT72LnYl<-qx8;-HV7Q-&Xdx%1a,hC=0u+HlsV>nuIQL-5" + "_>@kXQtMacfD.m-VAb8;IReM3$wf0''hra*so568'Ip&vRs849'MRYSp%:t:h5qSgwpEr$B>Q,;s(C#$)`svQuF$##-D,##,g68@2[T;.XSdN9Qe)rpt._K-#5wF)sP'##p#C0c%-Gb%" + "hd+<-j'Ai*x&&HMkT]C'OSl##5RG[JXaHN;d'uA#x._U;.`PU@(Z3dt4r152@:v,'R.Sj'w#0<-;kPI)FfJ&#AYJ&#//)>-k=m=*XnK$>=)72L]0I%>.G690a:$##<,);?;72#?x9+d;" + "^V'9;jY@;)br#q^YQpx:X#Te$Z^'=-=bGhLf:D6&bNwZ9-ZD#n^9HhLMr5G;']d&6'wYmTFmLq9wI>P(9mI[>kC-ekLC/R&CH+s'B;K-M6$EB%is00:" + "+A4[7xks.LrNk0&E)wILYF@2L'0Nb$+pv<(2.768/FrY&h$^3i&@+G%JT'<-,v`3;_)I9M^AE]CN?Cl2AZg+%4iTpT3$U4O]GKx'm9)b@p7YsvK3w^YR-" + "CdQ*:Ir<($u&)#(&?L9Rg3H)4fiEp^iI9O8KnTj,]H?D*r7'M;PwZ9K0E^k&-cpI;.p/6_vwoFMV<->#%Xi.LxVnrU(4&8/P+:hLSKj$#U%]49t'I:rgMi'FL@a:0Y-uA[39',(vbma*" + "hU%<-SRF`Tt:542R_VV$p@[p8DV[A,?1839FWdFTi1O*H&#(AL8[_P%.M>v^-))qOT*F5Cq0`Ye%+$B6i:7@0IXSsDiWP,##P`%/L-" + "S(qw%sf/@%#B6;/U7K]uZbi^Oc^2n%t<)'mEVE''n`WnJra$^TKvX5B>;_aSEK',(hwa0:i4G?.Bci.(X[?b*($,=-n<.Q%`(X=?+@Am*Js0&=3bh8K]mL69=Lb,OcZV/);TTm8VI;?%OtJ<(b4mq7M6:u?KRdFl*:xP?Yb.5)%w_I?7uk5JC+FS(m#i'k.'a0i)9<7b'fs'59hq$*5Uhv##pi^8+hIEBF`nvo`;'l0.^S1<-wUK2/Coh58KKhLj" + "M=SO*rfO`+qC`W-On.=AJ56>>i2@2LH6A:&5q`?9I3@@'04&p2/LVa*T-4<-i3;M9UvZd+N7>b*eIwg:CC)c<>nO&#$(>.Z-I&J(Q0Hd5Q%7Co-b`-cP)hI;*_F]u`Rb[.j8_Q/<&>uu+VsH$sM9TA%?)(vmJ80),P7E>)tjD%2L=-t#fK[%`v=Q8WlA2);Sa" + ">gXm8YB`1d@K#n]76-a$U,mF%Ul:#/'xoFM9QX-$.QN'>" + "[%$Z$uF6pA6Ki2O5:8w*vP1<-1`[G,)-m#>0`P&#eb#.3i)rtB61(o'$?X3B2Qft^ae_5tKL9MUe9b*sLEQ95C&`=G?@Mj=wh*'3E>=-<)Gt*Iw)'QG:`@I" + "wOf7&]1i'S01B+Ev/Nac#9S;=;YQpg_6U`*kVY39xK,[/6Aj7:'1Bm-_1EYfa1+o&o4hp7KN_Q(OlIo@S%;jVdn0'1h19w,WQhLI)3S#f$2(eb,jr*b;3Vw]*7NH%$c4Vs,eD9>XW8?N]o+(*pgC%/72LV-uW%iewS8W6m2rtCpo'RS1R84=@paTKt)>=%&1[)*vp'u+x,VrwN;&]kuO9JDbg=pO$J*.jVe;u'm0dr9l,<*wMK*Oe=g8lV_KEBFkO'oU]^=[-792#ok,)" + "i]lR8qQ2oA8wcRCZ^7w/Njh;?.stX?Q1>S1q4Bn$)K1<-rGdO'$Wr.Lc.CG)$/*JL4tNR/,SVO3,aUw'DJN:)Ss;wGn9A32ijw%FL+Z0Fn.U9;reSq)bmI32U==5ALuG&#Vf1398/pVo" + "1*c-(aY168o<`JsSbk-,1N;$>0:OUas(3:8Z972LSfF8eb=c-;>SPw7.6hn3m`9^Xkn(r.qS[0;T%&Qc=+STRxX'q1BNk3&*eu2;&8q$&x>Q#Q7^Tf+6<(d%ZVmj2bDi%.3L2n+4W'$P" + "iDDG)g,r%+?,$@?uou5tSe2aN_AQU*'IAO" + "URQ##V^Fv-XFbGM7Fl(N<3DhLGF%q.1rC$#:T__&Pi68%0xi_&[qFJ(77j_&JWoF.V735&T,[R*:xFR*K5>>#`bW-?4Ne_&6Ne_&6Ne_&n`kr-#GJcM6X;uM6X;uM(.a..^2TkL%oR(#" + ";u.T%fAr%4tJ8&><1=GHZ_+m9/#H1F^R#SC#*N=BA9(D?v[UiFY>>^8p,KKF.W]L29uLkLlu/+4T" + "w$)F./^n3+rlo+DB;5sIYGNk+i1t-69Jg--0pao7Sm#K)pdHW&;LuDNH@H>#/X-TI(;P>#,Gc>#0Su>#4`1?#8lC?#xL$#B.`$#F:r$#JF.%#NR@%#R_R%#Vke%#Zww%#_-4^Rh%Sflr-k'MS.o?.5/sWel/wpEM0%3'/1)K^f1-d>G21&v(35>V`39V7A4=onx4" + "A1OY5EI0;6Ibgr6M$HS7Q<)58C5w,;WoA*#[%T*#`1g*#d=#+#hI5+#lUG+#pbY+#tnl+#x$),#&1;,#*=M,#.I`,#2Ur,#6b.-#;w[H#iQtA#m^0B#qjBB#uvTB##-hB#'9$C#+E6C#" + "/QHC#3^ZC#7jmC#;v)D#?,)4kMYD4lVu`4m`:&5niUA5@(A5BA1]PBB:xlBCC=2CDLXMCEUtiCf&0g2'tN?PGT4CPGT4CPGT4CPGT4CPGT4CPGT4CPGT4CP" + "GT4CPGT4CPGT4CPGT4CPGT4CPGT4CP-qekC`.9kEg^+F$kwViFJTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5o,^<-28ZI'O?;xp" + "O?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xp;7q-#lLYI:xvD=#"; + +static const char* GetDefaultCompressedFontDataTTFBase85() +{ + return proggy_clean_ttf_compressed_data_base85; +} diff --git a/attachments/simple_engine/imgui/imgui_internal.h b/attachments/simple_engine/imgui/imgui_internal.h new file mode 100644 index 00000000..221e5d46 --- /dev/null +++ b/attachments/simple_engine/imgui/imgui_internal.h @@ -0,0 +1,1157 @@ +// dear imgui, v1.60 WIP +// (internals) + +// You may use this file to debug, understand or extend ImGui features but we don't provide any guarantee of forward compatibility! +// Set: +// #define IMGUI_DEFINE_MATH_OPERATORS +// To implement maths operators for ImVec2 (disabled by default to not collide with using IM_VEC2_CLASS_EXTRA along with your own math types+operators) + +#pragma once + +#ifndef IMGUI_VERSION +#error Must include imgui.h before imgui_internal.h +#endif + +#include // FILE* +#include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf +#include // INT_MIN, INT_MAX + +#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) +#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" +#endif + +//----------------------------------------------------------------------------- +// Forward Declarations +//----------------------------------------------------------------------------- + +struct ImRect; +struct ImGuiColMod; +struct ImGuiStyleMod; +struct ImGuiGroupData; +struct ImGuiMenuColumns; +struct ImGuiDrawContext; +struct ImGuiTextEditState; +struct ImGuiPopupRef; +struct ImGuiWindow; +struct ImGuiWindowSettings; + +typedef int ImGuiLayoutType; // enum: horizontal or vertical // enum ImGuiLayoutType_ +typedef int ImGuiButtonFlags; // flags: for ButtonEx(), ButtonBehavior() // enum ImGuiButtonFlags_ +typedef int ImGuiItemFlags; // flags: for PushItemFlag() // enum ImGuiItemFlags_ +typedef int ImGuiItemStatusFlags; // flags: storage for DC.LastItemXXX // enum ImGuiItemStatusFlags_ +typedef int ImGuiNavHighlightFlags; // flags: for RenderNavHighlight() // enum ImGuiNavHighlightFlags_ +typedef int ImGuiNavDirSourceFlags; // flags: for GetNavInputAmount2d() // enum ImGuiNavDirSourceFlags_ +typedef int ImGuiSeparatorFlags; // flags: for Separator() - internal // enum ImGuiSeparatorFlags_ +typedef int ImGuiSliderFlags; // flags: for SliderBehavior() // enum ImGuiSliderFlags_ + +//------------------------------------------------------------------------- +// STB libraries +//------------------------------------------------------------------------- + +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 +#include "stb_textedit.h" + +} // namespace ImGuiStb + +//----------------------------------------------------------------------------- +// Context +//----------------------------------------------------------------------------- + +#ifndef GImGui +extern IMGUI_API ImGuiContext* GImGui; // Current implicit ImGui context pointer +#endif + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +#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 + +// Helpers: Misc +IMGUI_API ImU32 ImHash(const void* data, int data_size, ImU32 seed = 0); // Pass data_size==0 for zero-terminated strings +IMGUI_API void* ImFileLoadToMemory(const char* filename, const char* file_open_mode, int* out_file_size = NULL, int padding_bytes = 0); +IMGUI_API FILE* ImFileOpen(const char* filename, const char* file_open_mode); +static inline bool ImCharIsSpace(int c) { return c == ' ' || c == '\t' || c == 0x3000; } +static inline bool ImIsPowerOfTwo(int v) { return v != 0 && (v & (v - 1)) == 0; } +static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } + +// Helpers: Geometry +IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p); +IMGUI_API bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w); + +// Helpers: String +IMGUI_API int ImStricmp(const char* str1, const char* str2); +IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); +IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); +IMGUI_API char* ImStrdup(const char* str); +IMGUI_API char* ImStrchrRange(const char* str_begin, const char* str_end, char c); +IMGUI_API int ImStrlenW(const ImWchar* str); +IMGUI_API const ImWchar*ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin); // Find beginning-of-line +IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); +IMGUI_API int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) IM_FMTARGS(3); +IMGUI_API int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) IM_FMTLIST(3); + +// Helpers: Math +// We are keeping those not leaking to the user by default, in the case the user has implicit cast operators between ImVec2 and its own types (when IM_VEC2_CLASS_EXTRA is defined) +#ifdef IMGUI_DEFINE_MATH_OPERATORS +static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x*rhs, lhs.y*rhs); } +static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { 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); } + +// 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; } } + +//----------------------------------------------------------------------------- +// Types +//----------------------------------------------------------------------------- + +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 +}; + +enum ImGuiSliderFlags_ +{ + 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. +}; + +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 +}; + +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 +}; + +// Storage for LastItem data +enum ImGuiItemStatusFlags_ +{ + 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 +}; + +enum ImGuiAxis +{ + ImGuiAxis_None = -1, + ImGuiAxis_X = 0, + ImGuiAxis_Y = 1 +}; + +enum ImGuiPlotType +{ + ImGuiPlotType_Lines, + ImGuiPlotType_Histogram +}; + +enum ImGuiDataType +{ + 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_ +}; + +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_, +}; + +// FIXME-NAV: Clarify/expose various repeat delay/rate +enum ImGuiInputReadMode +{ + 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 +}; + +enum ImGuiNavDirSourceFlags_ +{ + ImGuiNavDirSourceFlags_Keyboard = 1 << 0, + ImGuiNavDirSourceFlags_PadDPad = 1 << 1, + ImGuiNavDirSourceFlags_PadLStick = 1 << 2 +}; + +enum ImGuiNavForward +{ + 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; } +}; + +// Stacked color modifier, backup of modified data so we can restore it +struct ImGuiColMod +{ + 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; } +}; + +// Stacked data for BeginGroup()/EndGroup() +struct ImGuiGroupData +{ + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + float BackupIndentX; + float BackupGroupOffsetX; + float BackupCurrentLineHeight; + float BackupCurrentLineTextBaseOffset; + float BackupLogLinePosY; + bool BackupActiveIdIsAlive; + bool AdvanceCursor; +}; + +// Simple column measurement currently used for MenuItem() only. This is very short-sighted/throw-away code and NOT a generic helper. +struct IMGUI_API ImGuiMenuColumns +{ + 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); +}; + +// 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; } +}; + +struct ImGuiSettingsHandler +{ + const char* TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' + ImGuiID TypeHash; // == ImHash(TypeName, 0, 0) + void* (*ReadOpenFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name); + void (*ReadLineFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line); + void (*WriteAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf); + void* UserData; + + ImGuiSettingsHandler() { memset(this, 0, sizeof(*this)); } +}; + +// Storage for current popup stack +struct ImGuiPopupRef +{ + ImGuiID PopupId; // Set on OpenPopup() + ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() + ImGuiWindow* ParentWindow; // Set on OpenPopup() + int OpenFrameCount; // Set on OpenPopup() + ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differenciate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) + ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) + ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup +}; + +struct ImGuiColumnData +{ + float OffsetNorm; // Column start offset, normalized 0.0 (far left) -> 1.0 (far right) + float OffsetNormBeforeResize; + ImGuiColumnsFlags Flags; // Not exposed + ImRect ClipRect; + + ImGuiColumnData() { OffsetNorm = OffsetNormBeforeResize = 0.0f; Flags = 0; } +}; + +struct ImGuiColumnsSet +{ + ImGuiID ID; + ImGuiColumnsFlags Flags; + bool IsFirstFrame; + bool IsBeingResized; + int Current; + int Count; + float MinX, MaxX; + float StartPosY; + float StartMaxPosX; // Backup of CursorMaxPos + float CellMinY, CellMaxY; + ImVector Columns; + + ImGuiColumnsSet() { Clear(); } + void Clear() + { + ID = 0; + Flags = 0; + IsFirstFrame = false; + IsBeingResized = false; + Current = 0; + Count = 1; + MinX = MaxX = 0.0f; + StartPosY = 0.0f; + StartMaxPosX = 0.0f; + CellMinY = CellMaxY = 0.0f; + Columns.clear(); + } +}; + +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() + + // Const data + // FIXME: Bake rounded corners fill/borders in atlas + ImVec2 CircleVtx12[12]; + + 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(); +}; + +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(); } +}; + +// 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; + } +}; + +// 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)); + } +}; + +// Transient per-window flags, reset at the beginning of the frame. For child window, inherited from parent on first Begin(). +// This is going to be exposed in imgui.h when stabilized enough. +enum ImGuiItemFlags_ +{ + ImGuiItemFlags_AllowKeyboardFocus = 1 << 0, // true + ImGuiItemFlags_ButtonRepeat = 1 << 1, // false // Button() will return true multiple times based on io.KeyRepeatDelay and io.KeyRepeatRate settings. + ImGuiItemFlags_Disabled = 1 << 2, // false // FIXME-WIP: Disable interactions but doesn't affect visuals. Should be: grey out and disable interactions with widgets that affect data + view widgets (WIP) + ImGuiItemFlags_NoNav = 1 << 3, // false + ImGuiItemFlags_NoNavDefaultFocus = 1 << 4, // false + ImGuiItemFlags_SelectableDontClosePopup = 1 << 5, // false // MenuItem/Selectable() automatically closes current Popup window + ImGuiItemFlags_Default_ = ImGuiItemFlags_AllowKeyboardFocus +}; + +// Transient per-window data, reset at the beginning of the frame +// FIXME: That's theory, in practice the delimitation between ImGuiWindow and ImGuiDrawContext is quite tenuous and could be reconsidered. +struct IMGUI_API ImGuiDrawContext +{ + ImVec2 CursorPos; + ImVec2 CursorPosPrevLine; + ImVec2 CursorStartPos; + ImVec2 CursorMaxPos; // Used to implicitly calculate the size of our contents, always growing during the frame. Turned into window->SizeContents at the beginning of next frame + float CurrentLineHeight; + float CurrentLineTextBaseOffset; + float PrevLineHeight; + float PrevLineTextBaseOffset; + float LogLinePosY; + int TreeDepth; + ImU32 TreeDepthMayJumpToParentOnPop; // Store a copy of !g.NavIdIsAlive for TreeDepth 0..31 + ImGuiID LastItemId; + ImGuiItemStatusFlags LastItemStatusFlags; + ImRect LastItemRect; // Interaction rect + ImRect LastItemDisplayRect; // End-user display rect (only valid if LastItemStatusFlags & ImGuiItemStatusFlags_HasDisplayRect) + bool NavHideHighlightOneFrame; + bool NavHasScroll; // Set when scrolling can be used (ScrollMax > 0.0f) + int NavLayerCurrent; // Current layer, 0..31 (we currently only use 0..1) + int NavLayerCurrentMask; // = (1 << NavLayerCurrent) used by ItemAdd prior to clipping. + int NavLayerActiveMask; // Which layer have been written to (result from previous frame) + int NavLayerActiveMaskNext; // Which layer have been written to (buffer for current frame) + bool MenuBarAppending; // FIXME: Remove this + float MenuBarOffsetX; + ImVector ChildWindows; + ImGuiStorage* StateStorage; + ImGuiLayoutType LayoutType; + ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() + + // We store the current settings outside of the vectors to increase memory locality (reduce cache misses). The vectors are rarely modified. Also it allows us to not heap allocate for short-lived windows which are not using those settings. + ImGuiItemFlags ItemFlags; // == ItemFlagsStack.back() [empty == ImGuiItemFlags_Default] + float ItemWidth; // == ItemWidthStack.back(). 0.0: default, >0.0: width in pixels, <0.0: align xx pixels to the right of window + float TextWrapPos; // == TextWrapPosStack.back() [empty == -1.0f] + 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; + } +}; + +// 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()); } +}; + +// 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; } +}; + +//----------------------------------------------------------------------------- +// Internal API +// No guarantee of forward compatibility here. +//----------------------------------------------------------------------------- + +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); + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef _MSC_VER +#pragma warning (pop) +#endif diff --git a/attachments/simple_engine/imgui/stb_rect_pack.h b/attachments/simple_engine/imgui/stb_rect_pack.h new file mode 100644 index 00000000..fafd8897 --- /dev/null +++ b/attachments/simple_engine/imgui/stb_rect_pack.h @@ -0,0 +1,573 @@ +// stb_rect_pack.h - v0.08 - public domain - rectangle packing +// Sean Barrett 2014 +// +// Useful for e.g. packing rectangular textures into an atlas. +// Does not do rotation. +// +// Not necessarily the awesomest packing method, but better than +// the totally naive one in stb_truetype (which is primarily what +// this is meant to replace). +// +// Has only had a few tests run, may have issues. +// +// More docs to come. +// +// No memory allocations; uses qsort() and assert() from stdlib. +// Can override those by defining STBRP_SORT and STBRP_ASSERT. +// +// This library currently uses the Skyline Bottom-Left algorithm. +// +// Please note: better rectangle packers are welcome! Please +// implement them to the same API, but with a different init +// function. +// +// Credits +// +// Library +// Sean Barrett +// Minor features +// Martins Mozeiko +// Bugfixes / warning fixes +// Jeremy Jaussaud +// +// Version history: +// +// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) +// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) +// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort +// 0.05: added STBRP_ASSERT to allow replacing assert +// 0.04: fixed minor bug in STBRP_LARGE_RECTS support +// 0.01: initial release +// +// LICENSE +// +// This software is in the public domain. Where that dedication is not +// recognized, you are granted a perpetual, irrevocable license to copy, +// distribute, and modify this file as you see fit. + +////////////////////////////////////////////////////////////////////////////// +// +// INCLUDE SECTION +// + +#ifndef STB_INCLUDE_STB_RECT_PACK_H +#define STB_INCLUDE_STB_RECT_PACK_H + +#define STB_RECT_PACK_VERSION 1 + +#ifdef STBRP_STATIC +#define STBRP_DEF static +#else +#define STBRP_DEF extern +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +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; +#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' +}; + +#ifdef __cplusplus +} +#endif + +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// IMPLEMENTATION SECTION +// + +#ifdef STB_RECT_PACK_IMPLEMENTATION +#ifndef STBRP_SORT +#include +#define STBRP_SORT qsort +#endif + +#ifndef STBRP_ASSERT +#include +#define STBRP_ASSERT assert +#endif + +enum +{ + 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); + } +} + +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; + } +} + +STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) +{ + int i; +#ifndef STBRP_LARGE_RECTS + 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; +#ifdef STBRP_LARGE_RECTS + context->extra[1].y = (1<<30); +#else + context->extra[1].y = 65535; +#endif + 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); + + #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; +} + +typedef struct +{ + 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; +} + +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); + +#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); + } +#endif + + 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); +} + +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); +} + +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); +} + +#ifdef STBRP_LARGE_RECTS +#define STBRP__MAXVAL 0xffffffff +#else +#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); +} +#endif diff --git a/attachments/simple_engine/imgui/stb_textedit.h b/attachments/simple_engine/imgui/stb_textedit.h new file mode 100644 index 00000000..3c300325 --- /dev/null +++ b/attachments/simple_engine/imgui/stb_textedit.h @@ -0,0 +1,1317 @@ +// [ImGui] this is a slightly modified version of stb_truetype.h 1.8 +// [ImGui] - fixed some minor warnings +// [ImGui] - added STB_TEXTEDIT_MOVEWORDLEFT/STB_TEXTEDIT_MOVEWORDRIGHT custom handler (#473) + +// stb_textedit.h - v1.8 - public domain - Sean Barrett +// Development of this library was sponsored by RAD Game Tools +// +// This C header file implements the guts of a multi-line text-editing +// widget; you implement display, word-wrapping, and low-level string +// insertion/deletion, and stb_textedit will map user inputs into +// insertions & deletions, plus updates to the cursor position, +// selection state, and undo state. +// +// It is intended for use in games and other systems that need to build +// their own custom widgets and which do not have heavy text-editing +// requirements (this library is not recommended for use for editing large +// texts, as its performance does not scale and it has limited undo). +// +// Non-trivial behaviors are modelled after Windows text controls. +// +// +// LICENSE +// +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// +// DEPENDENCIES +// +// Uses the C runtime function 'memmove', which you can override +// by defining STB_TEXTEDIT_memmove before the implementation. +// Uses no other functions. Performs no runtime allocations. +// +// +// VERSION HISTORY +// +// 1.8 (2016-04-02) better keyboard handling when mouse button is down +// 1.7 (2015-09-13) change y range handling in case baseline is non-0 +// 1.6 (2015-04-15) allow STB_TEXTEDIT_memmove +// 1.5 (2014-09-10) add support for secondary keys for OS X +// 1.4 (2014-08-17) fix signed/unsigned warnings +// 1.3 (2014-06-19) fix mouse clicking to round to nearest char boundary +// 1.2 (2014-05-27) fix some RAD types that had crept into the new code +// 1.1 (2013-12-15) move-by-word (requires STB_TEXTEDIT_IS_SPACE ) +// 1.0 (2012-07-26) improve documentation, initial public release +// 0.3 (2012-02-24) bugfixes, single-line mode; insert mode +// 0.2 (2011-11-28) fixes to undo/redo +// 0.1 (2010-07-08) initial version +// +// ADDITIONAL CONTRIBUTORS +// +// Ulf Winklemann: move-by-word in 1.1 +// Fabian Giesen: secondary key inputs in 1.5 +// Martins Mozeiko: STB_TEXTEDIT_memmove +// +// Bugfixes: +// Scott Graham +// Daniel Keller +// Omar Cornut +// +// USAGE +// +// This file behaves differently depending on what symbols you define +// before including it. +// +// +// Header-file mode: +// +// If you do not define STB_TEXTEDIT_IMPLEMENTATION before including this, +// it will operate in "header file" mode. In this mode, it declares a +// single public symbol, STB_TexteditState, which encapsulates the current +// state of a text widget (except for the string, which you will store +// separately). +// +// To compile in this mode, you must define STB_TEXTEDIT_CHARTYPE to a +// primitive type that defines a single character (e.g. char, wchar_t, etc). +// +// To save space or increase undo-ability, you can optionally define the +// following things that are used by the undo system: +// +// STB_TEXTEDIT_POSITIONTYPE small int type encoding a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +// If you don't define these, they are set to permissive types and +// moderate sizes. The undo system does no memory allocations, so +// it grows STB_TexteditState by the worst-case storage which is (in bytes): +// +// [4 + sizeof(STB_TEXTEDIT_POSITIONTYPE)] * STB_TEXTEDIT_UNDOSTATE_COUNT +// + sizeof(STB_TEXTEDIT_CHARTYPE) * STB_TEXTEDIT_UNDOCHAR_COUNT +// +// +// Implementation mode: +// +// If you define STB_TEXTEDIT_IMPLEMENTATION before including this, it +// will compile the implementation of the text edit widget, depending +// on a large number of symbols which must be defined before the include. +// +// The implementation is defined only as static functions. You will then +// need to provide your own APIs in the same file which will access the +// static functions. +// +// The basic concept is that you provide a "string" object which +// behaves like an array of characters. stb_textedit uses indices to +// refer to positions in the string, implicitly representing positions +// in the displayed textedit. This is true for both plain text and +// rich text; even with rich text stb_truetype interacts with your +// code as if there was an array of all the displayed characters. +// +// Symbols that must be the same in header-file and implementation mode: +// +// STB_TEXTEDIT_CHARTYPE the character type +// STB_TEXTEDIT_POSITIONTYPE small type that a valid cursor position +// STB_TEXTEDIT_UNDOSTATECOUNT the number of undo states to allow +// STB_TEXTEDIT_UNDOCHARCOUNT the number of characters to store in the undo buffer +// +// Symbols you must define for implementation mode: +// +// STB_TEXTEDIT_STRING the type of object representing a string being edited, +// typically this is a wrapper object with other data you need +// +// STB_TEXTEDIT_STRINGLEN(obj) the length of the string (ideally O(1)) +// STB_TEXTEDIT_LAYOUTROW(&r,obj,n) returns the results of laying out a line of characters +// starting from character #n (see discussion below) +// STB_TEXTEDIT_GETWIDTH(obj,n,i) returns the pixel delta from the xpos of the i'th character +// to the xpos of the i+1'th char for a line of characters +// starting at character #n (i.e. accounts for kerning +// with previous char) +// STB_TEXTEDIT_KEYTOTEXT(k) maps a keyboard input to an insertable character +// (return type is int, -1 means not valid to insert) +// STB_TEXTEDIT_GETCHAR(obj,i) returns the i'th character of obj, 0-based +// STB_TEXTEDIT_NEWLINE the character returned by _GETCHAR() we recognize +// as manually wordwrapping for end-of-line positioning +// +// STB_TEXTEDIT_DELETECHARS(obj,i,n) delete n characters starting at i +// STB_TEXTEDIT_INSERTCHARS(obj,i,c*,n) insert n characters at i (pointed to by STB_TEXTEDIT_CHARTYPE*) +// +// STB_TEXTEDIT_K_SHIFT a power of two that is or'd in to a keyboard input to represent the shift key +// +// STB_TEXTEDIT_K_LEFT keyboard input to move cursor left +// STB_TEXTEDIT_K_RIGHT keyboard input to move cursor right +// STB_TEXTEDIT_K_UP keyboard input to move cursor up +// STB_TEXTEDIT_K_DOWN keyboard input to move cursor down +// STB_TEXTEDIT_K_LINESTART keyboard input to move cursor to start of line // e.g. HOME +// STB_TEXTEDIT_K_LINEEND keyboard input to move cursor to end of line // e.g. END +// STB_TEXTEDIT_K_TEXTSTART keyboard input to move cursor to start of text // e.g. ctrl-HOME +// STB_TEXTEDIT_K_TEXTEND keyboard input to move cursor to end of text // e.g. ctrl-END +// STB_TEXTEDIT_K_DELETE keyboard input to delete selection or character under cursor +// STB_TEXTEDIT_K_BACKSPACE keyboard input to delete selection or character left of cursor +// STB_TEXTEDIT_K_UNDO keyboard input to perform undo +// STB_TEXTEDIT_K_REDO keyboard input to perform redo +// +// Optional: +// STB_TEXTEDIT_K_INSERT keyboard input to toggle insert mode +// STB_TEXTEDIT_IS_SPACE(ch) true if character is whitespace (e.g. 'isspace'), +// required for default WORDLEFT/WORDRIGHT handlers +// STB_TEXTEDIT_MOVEWORDLEFT(obj,i) custom handler for WORDLEFT, returns index to move cursor to +// STB_TEXTEDIT_MOVEWORDRIGHT(obj,i) custom handler for WORDRIGHT, returns index to move cursor to +// STB_TEXTEDIT_K_WORDLEFT keyboard input to move cursor left one word // e.g. ctrl-LEFT +// STB_TEXTEDIT_K_WORDRIGHT keyboard input to move cursor right one word // e.g. ctrl-RIGHT +// STB_TEXTEDIT_K_LINESTART2 secondary keyboard input to move cursor to start of line +// STB_TEXTEDIT_K_LINEEND2 secondary keyboard input to move cursor to end of line +// STB_TEXTEDIT_K_TEXTSTART2 secondary keyboard input to move cursor to start of text +// STB_TEXTEDIT_K_TEXTEND2 secondary keyboard input to move cursor to end of text +// +// Todo: +// STB_TEXTEDIT_K_PGUP keyboard input to move cursor up a page +// STB_TEXTEDIT_K_PGDOWN keyboard input to move cursor down a page +// +// Keyboard input must be encoded as a single integer value; e.g. a character code +// and some bitflags that represent shift states. to simplify the interface, SHIFT must +// be a bitflag, so we can test the shifted state of cursor movements to allow selection, +// i.e. (STB_TEXTED_K_RIGHT|STB_TEXTEDIT_K_SHIFT) should be shifted right-arrow. +// +// You can encode other things, such as CONTROL or ALT, in additional bits, and +// then test for their presence in e.g. STB_TEXTEDIT_K_WORDLEFT. For example, +// my Windows implementations add an additional CONTROL bit, and an additional KEYDOWN +// bit. Then all of the STB_TEXTEDIT_K_ values bitwise-or in the KEYDOWN bit, +// and I pass both WM_KEYDOWN and WM_CHAR events to the "key" function in the +// API below. The control keys will only match WM_KEYDOWN events because of the +// keydown bit I add, and STB_TEXTEDIT_KEYTOTEXT only tests for the KEYDOWN +// bit so it only decodes WM_CHAR events. +// +// STB_TEXTEDIT_LAYOUTROW returns information about the shape of one displayed +// row of characters assuming they start on the i'th character--the width and +// the height and the number of characters consumed. This allows this library +// to traverse the entire layout incrementally. You need to compute word-wrapping +// here. +// +// Each textfield keeps its own insert mode state, which is not how normal +// applications work. To keep an app-wide insert mode, update/copy the +// "insert_mode" field of STB_TexteditState before/after calling API functions. +// +// API +// +// void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) +// +// void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +// void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +// int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +// int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) +// void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int key) +// +// Each of these functions potentially updates the string and updates the +// state. +// +// initialize_state: +// set the textedit state to a known good default state when initially +// constructing the textedit. +// +// click: +// 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 +// do both (as in Win32), or distinguish Unicode characters from control +// inputs, set a high bit to distinguish the two; then you can define the +// 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. +// +// +// Notes: +// +// This is designed to be usable in IMGUI, so it allows for the possibility of +// running in an IMGUI that has NOT cached the multi-line layout. For this +// reason, it provides an interface that is compatible with computing the +// layout incrementally--we try to make sure we make as few passes through +// as possible. (For example, to locate the mouse pointer in the text, we +// could define functions that return the X and Y positions of characters +// and binary search Y and then X, but if we're doing dynamic layout this +// will run the layout algorithm many times, so instead we manually search +// forward in one pass. Similar logic applies to e.g. up-arrow and +// down-arrow movement.) +// +// If it's run in a widget that *has* cached the layout, then this is less +// efficient, but it's not horrible on modern computers. But you wouldn't +// want to edit million-line files with it. + + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//// +//// Header-file mode +//// +//// + +#ifndef INCLUDE_STB_TEXTEDIT_H +#define INCLUDE_STB_TEXTEDIT_H + +//////////////////////////////////////////////////////////////////////// +// +// STB_TexteditState +// +// Definition of STB_TexteditState which you should store +// per-textfield; it includes cursor position, selection state, +// and undo state. +// + +#ifndef STB_TEXTEDIT_UNDOSTATECOUNT +#define STB_TEXTEDIT_UNDOSTATECOUNT 99 +#endif +#ifndef STB_TEXTEDIT_UNDOCHARCOUNT +#define STB_TEXTEDIT_UNDOCHARCOUNT 999 +#endif +#ifndef STB_TEXTEDIT_CHARTYPE +#define STB_TEXTEDIT_CHARTYPE int +#endif +#ifndef STB_TEXTEDIT_POSITIONTYPE +#define STB_TEXTEDIT_POSITIONTYPE int +#endif + +typedef struct +{ + // 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; +} 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; +} STB_TexteditState; + + +//////////////////////////////////////////////////////////////////////// +// +// StbTexteditRow +// +// Result of layout query, used by stb_textedit to determine where +// the text in each row is. + +// 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; +} StbTexteditRow; +#endif //INCLUDE_STB_TEXTEDIT_H + + +//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// +//// +//// Implementation mode +//// +//// + + +// 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 +#endif + + +///////////////////////////////////////////////////////////////////////////// +// +// Mouse input handling +// + +// 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; +} + +// 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; +} + +// 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; +} + +///////////////////////////////////////////////////////////////////////////// +// +// Keyboard input handling +// + +// forward declarations +static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length); +static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length); +static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length); + +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 +} 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); +} + +#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; +} + +// 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; +} + +// 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; + } +} + +// 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; + } +} + +// 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; + } +} + +// 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; + } +} + +#ifdef STB_TEXTEDIT_IS_SPACE +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; +} + +#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; + + if( c < 0 ) + c = 0; + + return c; +} +#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 ) +{ + const int len = STB_TEXTEDIT_STRINGLEN(_str); + while( c < len && !is_word_boundary( _str, c ) ) + ++c; + + if( c > len ) + c = len; + + return c; +} +#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; +} + +// 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; +} + +// 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; +} + +// 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; + } + +#ifdef STB_TEXTEDIT_K_INSERT + 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; + +#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; +#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; +#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; + +#ifdef 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; + +#ifdef 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; + +#ifdef STB_TEXTEDIT_K_TEXTSTART2 + 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; + +#ifdef STB_TEXTEDIT_K_TEXTEND2 + 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; + + +#ifdef 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; + } + +#ifdef 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; + } + +#ifdef STB_TEXTEDIT_K_LINESTART2 + 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; + } + +#ifdef STB_TEXTEDIT_K_LINEEND2 + 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 + } +} + +///////////////////////////////////////////////////////////////////////////// +// +// Undo processing +// +// @OPTIMIZE: the undo/redo buffer should be circular + +static void stb_textedit_flush_redo(StbUndoState *state) +{ + 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]))); + } +} + +// discard the oldest entry in the redo list--it's bad if this +// ever happens, but because undo & redo have to store the actual +// characters in different cases, the redo character buffer can +// 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]))); + } +} + +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++]; +} + +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]; + } +} + +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--; +} + +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++; +} + +static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int 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); + } +} + +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); + } +} + +// 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; +} + +// API initialize +static void stb_textedit_initialize_state(STB_TexteditState *state, int is_single_line) +{ + stb_textedit_clear_state(state, is_single_line); +} +#endif//STB_TEXTEDIT_IMPLEMENTATION diff --git a/attachments/simple_engine/imgui/stb_truetype.h b/attachments/simple_engine/imgui/stb_truetype.h new file mode 100644 index 00000000..e6dae975 --- /dev/null +++ b/attachments/simple_engine/imgui/stb_truetype.h @@ -0,0 +1,3263 @@ +// stb_truetype.h - v1.10 - public domain +// authored from 2009-2015 by Sean Barrett / RAD Game Tools +// +// This library processes TrueType files: +// parse files +// extract glyph metrics +// extract glyph shapes +// render glyphs to one-channel bitmaps with antialiasing (box filter) +// +// Todo: +// non-MS cmaps +// crashproof on bad data +// hinting? (no longer patented) +// cleartype-style AA? +// optimize: use simple memory allocator for intermediates +// optimize: build edge-list directly from curves +// optimize: rasterize directly from curves? +// +// ADDITIONAL CONTRIBUTORS +// +// Mikko Mononen: compound shape support, more cmap formats +// Tor Andersson: kerning, subpixel rendering +// +// Misc other: +// Ryan Gordon +// Simon Glass +// +// Bug/warning reports/fixes: +// "Zer" on mollyrocket (with fix) +// Cass Everitt +// stoiko (Haemimont Games) +// Brian Hook +// Walter van Niftrik +// David Gow +// David Given +// Ivan-Assen Ivanov +// Anthony Pesch +// Johan Duparc +// Hou Qiming +// Fabian "ryg" Giesen +// Martins Mozeiko +// Cap Petschulat +// Omar Cornut +// github:aloucks +// Peter LaValle +// Sergey Popov +// Giumo X. Clanjor +// Higor Euripedes +// Thomas Fields +// Derek Vinyard +// +// VERSION HISTORY +// +// 1.10 (2016-04-02) user-defined fabs(); rare memory leak; remove duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use allocation userdata properly +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// variant PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// 1.06 (2015-07-14) performance improvements (~35% faster on x86 and x64 on test machine) +// also more precise AA rasterizer, except if shapes overlap +// remove need for STBTT_sort +// 1.05 (2015-04-15) fix misplaced definitions for STBTT_STATIC +// 1.04 (2015-04-15) typo in example +// 1.03 (2015-04-12) STBTT_STATIC, fix memory leak in new packing, various fixes +// +// Full history can be found at the end of this file. +// +// LICENSE +// +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// USAGE +// +// Include this file in whatever places neeed to refer to it. In ONE C/C++ +// file, write: +// #define STB_TRUETYPE_IMPLEMENTATION +// before the #include of this file. This expands out the actual +// implementation into that C/C++ file. +// +// To make the implementation private to the file that generates the implementation, +// #define STBTT_STATIC +// +// Simple 3D API (don't ship this, but it's fine for tools and quick start) +// stbtt_BakeFontBitmap() -- bake a font to a bitmap for use as texture +// stbtt_GetBakedQuad() -- compute quad to draw for a given char +// +// Improved 3D API (more shippable): +// #include "stb_rect_pack.h" -- optional, but you really want it +// stbtt_PackBegin() +// stbtt_PackSetOversample() -- for improved quality on small fonts +// stbtt_PackFontRanges() -- pack and renders +// stbtt_PackEnd() +// stbtt_GetPackedQuad() +// +// "Load" a font file from a memory buffer (you have to keep the buffer loaded) +// stbtt_InitFont() +// stbtt_GetFontOffsetForIndex() -- use for TTC font collections +// +// Render a unicode codepoint to a bitmap +// stbtt_GetCodepointBitmap() -- allocates and returns a bitmap +// stbtt_MakeCodepointBitmap() -- renders into bitmap you provide +// stbtt_GetCodepointBitmapBox() -- how big the bitmap must be +// +// Character advance/positioning +// stbtt_GetCodepointHMetrics() +// stbtt_GetFontVMetrics() +// stbtt_GetCodepointKernAdvance() +// +// Starting with version 1.06, the rasterizer was replaced with a new, +// faster and generally-more-precise rasterizer. The new rasterizer more +// accurately measures pixel coverage for anti-aliasing, except in the case +// where multiple shapes overlap, in which case it overestimates the AA pixel +// coverage. Thus, anti-aliasing of intersecting shapes may look wrong. If +// this turns out to be a problem, you can re-enable the old rasterizer with +// #define STBTT_RASTERIZER_VERSION 1 +// which will incur about a 15% speed hit. +// +// ADDITIONAL DOCUMENTATION +// +// Immediately after this block comment are a series of sample programs. +// +// After the sample programs is the "header file" section. This section +// includes documentation for each API function. +// +// Some important concepts to understand to use this library: +// +// Codepoint +// Characters are defined by unicode codepoints, e.g. 65 is +// uppercase A, 231 is lowercase c with a cedilla, 0x7e30 is +// the hiragana for "ma". +// +// Glyph +// A visual character shape (every codepoint is rendered as +// some glyph) +// +// Glyph index +// A font-specific integer ID representing a glyph +// +// Baseline +// Glyph shapes are defined relative to a baseline, which is the +// bottom of uppercase characters. Characters extend both above +// and below the baseline. +// +// Current Point +// As you draw text to the screen, you keep track of a "current point" +// which is the origin of each character. The current point's vertical +// position is the baseline. Even "baked fonts" use this model. +// +// Vertical Font Metrics +// The vertical qualities of the font, used to vertically position +// and space the characters. See docs for stbtt_GetFontVMetrics. +// +// Font Size in Pixels or Points +// The preferred interface for specifying font sizes in stb_truetype +// is to specify how tall the font's vertical extent should be in pixels. +// If that sounds good enough, skip the next paragraph. +// +// Most font APIs instead use "points", which are a common typographic +// measurement for describing font size, defined as 72 points per inch. +// stb_truetype provides a point API for compatibility. However, true +// "per inch" conventions don't make much sense on computer displays +// since they different monitors have different number of pixels per +// inch. For example, Windows traditionally uses a convention that +// there are 96 pixels per inch, thus making 'inch' measurements have +// nothing to do with inches, and thus effectively defining a point to +// be 1.333 pixels. Additionally, the TrueType font data provides +// an explicit scale factor to scale a given font's glyphs to points, +// but the author has observed that this scale factor is often wrong +// for non-commercial fonts, thus making fonts scaled in points +// according to the TrueType spec incoherently sized in practice. +// +// ADVANCED USAGE +// +// Quality: +// +// - Use the functions with Subpixel at the end to allow your characters +// to have subpixel positioning. Since the font is anti-aliased, not +// hinted, this is very import for quality. (This is not possible with +// baked fonts.) +// +// - Kerning is now supported, and if you're supporting subpixel rendering +// then kerning is worth using to give your text a polished look. +// +// Performance: +// +// - Convert Unicode codepoints to glyph indexes and operate on the glyphs; +// if you don't do this, stb_truetype is forced to do the conversion on +// every call. +// +// - There are a lot of memory allocations. We should modify it to take +// a temp buffer and allocate from the temp buffer (without freeing), +// should help performance a lot. +// +// NOTES +// +// The system uses the raw data found in the .ttf file without changing it +// and without building auxiliary data structures. This is a bit inefficient +// on little-endian systems (the data is big-endian), but assuming you're +// caching the bitmaps or glyph shapes this shouldn't be a big deal. +// +// It appears to be very hard to programmatically determine what font a +// given file is in a general way. I provide an API for this, but I don't +// recommend it. +// +// +// SOURCE STATISTICS (based on v0.6c, 2050 LOC) +// +// Documentation & header file 520 LOC \___ 660 LOC documentation +// Sample code 140 LOC / +// Truetype parsing 620 LOC ---- 620 LOC TrueType +// Software rasterization 240 LOC \ . +// Curve tesselation 120 LOC \__ 550 LOC Bitmap creation +// Bitmap management 100 LOC / +// Baked bitmap interface 70 LOC / +// Font name matching & access 150 LOC ---- 150 +// C runtime library abstraction 60 LOC ---- 60 +// +// +// PERFORMANCE MEASUREMENTS FOR 1.06: +// +// 32-bit 64-bit +// Previous release: 8.83 s 7.68 s +// Pool allocations: 7.72 s 6.34 s +// Inline sort : 6.54 s 5.65 s +// New rasterizer : 5.63 s 5.00 s + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// SAMPLE PROGRAMS +//// +// +// 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" + +unsigned char ttf_buffer[1<<20]; +unsigned char temp_bitmap[512*512]; + +stbtt_bakedchar cdata[96]; // ASCII 32..126 is 95 glyphs +GLuint ftex; + +void my_stbtt_initfont(void) +{ + fread(ttf_buffer, 1, 1<<20, fopen("c:/windows/fonts/times.ttf", "rb")); + stbtt_BakeFontBitmap(ttf_buffer,0, 32.0, temp_bitmap,512,512, 32,96, cdata); // no guarantee this fits! + // can free ttf_buffer at this point + glGenTextures(1, &ftex); + glBindTexture(GL_TEXTURE_2D, ftex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, 512,512, 0, GL_ALPHA, GL_UNSIGNED_BYTE, temp_bitmap); + // can free temp_bitmap at this point + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); +} + +void my_stbtt_print(float x, float y, char *text) +{ + // assume orthographic projection with units = screen pixels, origin at top left + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, ftex); + glBegin(GL_QUADS); + while (*text) { + if (*text >= 32 && *text < 128) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(cdata, 512,512, *text-32, &x,&y,&q,1);//1=opengl & d3d10+,0=d3d9 + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y0); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y0); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y1); + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y1); + } + ++text; + } + glEnd(); +} +#endif +// +// +////////////////////////////////////////////////////////////////////////////// +// +// 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" + +char ttf_buffer[1<<25]; + +int main(int argc, char **argv) +{ + stbtt_fontinfo font; + unsigned char *bitmap; + int w,h,i,j,c = (argc > 1 ? atoi(argv[1]) : 'a'), s = (argc > 2 ? atoi(argv[2]) : 20); + + fread(ttf_buffer, 1, 1<<25, fopen(argc > 3 ? argv[3] : "c:/windows/fonts/arialbd.ttf", "rb")); + + stbtt_InitFont(&font, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer,0)); + bitmap = stbtt_GetCodepointBitmap(&font, 0,stbtt_ScaleForPixelHeight(&font, s), c, &w, &h, 0,0); + + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) + putchar(" .:ioVM@"[bitmap[j*w+i]>>5]); + putchar('\n'); + } + return 0; +} +#endif +// +// Output: +// +// .ii. +// @@@@@@. +// V@Mio@@o +// :i. V@V +// :oM@@M +// :@@@MM@M +// @@o o@M +// :@@. M@M +// @@@o@@@@ +// :M@@V:@@. +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program: print "Hello World!" banner, with bugs +// +#if 0 +char buffer[24<<20]; +unsigned char screen[20][79]; + +int main(int arg, char **argv) +{ + stbtt_fontinfo font; + int i,j,ascent,baseline,ch=0; + float scale, xpos=2; // leave a little padding in case the character extends left + char *text = "Heljo World!"; // intentionally misspelled to show 'lj' brokenness + + fread(buffer, 1, 1000000, fopen("c:/windows/fonts/arialbd.ttf", "rb")); + stbtt_InitFont(&font, buffer, 0); + + scale = stbtt_ScaleForPixelHeight(&font, 15); + stbtt_GetFontVMetrics(&font, &ascent,0,0); + baseline = (int) (ascent*scale); + + while (text[ch]) { + int advance,lsb,x0,y0,x1,y1; + float x_shift = xpos - (float) floor(xpos); + stbtt_GetCodepointHMetrics(&font, text[ch], &advance, &lsb); + stbtt_GetCodepointBitmapBoxSubpixel(&font, text[ch], scale,scale,x_shift,0, &x0,&y0,&x1,&y1); + stbtt_MakeCodepointBitmapSubpixel(&font, &screen[baseline + y0][(int) xpos + x0], x1-x0,y1-y0, 79, scale,scale,x_shift,0, text[ch]); + // note that this stomps the old data, so where character boxes overlap (e.g. 'lj') it's wrong + // because this API is really for baking character bitmaps into textures. if you want to render + // a sequence of characters, you really need to render each bitmap to a temp buffer, then + // "alpha blend" that into the working buffer + xpos += (advance * scale); + if (text[ch+1]) + xpos += scale*stbtt_GetCodepointKernAdvance(&font, text[ch],text[ch+1]); + ++ch; + } + + for (j=0; j < 20; ++j) { + for (i=0; i < 78; ++i) + putchar(" .:ioVM@"[screen[j][i]>>5]); + putchar('\n'); + } + + return 0; +} +#endif + + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// INTEGRATION WITH YOUR CODEBASE +//// +//// The following sections allow you to supply alternate definitions +//// 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 +#endif + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// INTERFACE +//// +//// + +#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); +// 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__ + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// IMPLEMENTATION +//// +//// + +#ifdef STB_TRUETYPE_IMPLEMENTATION + +#ifndef STBTT_MAX_OVERSAMPLE +#define STBTT_MAX_OVERSAMPLE 8 +#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]; + +#ifndef STBTT_RASTERIZER_VERSION +#define STBTT_RASTERIZER_VERSION 2 +#endif + +////////////////////////////////////////////////////////////////////////// +// +// accessors to parse data from file +// + +// 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) + +#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)) + +#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]; } + +#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]) + +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; +} + +// @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_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; +} + +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_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_DEF int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **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; +} + +static int stbtt__GetGlyfOffset(const stbtt_fontinfo *info, int glyph_index) +{ + 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 (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 +} + +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; + + 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); +} + +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; +} + +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_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_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_DEF void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *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); +} + +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); +} + +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; +} + +STBTT_DEF float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels) +{ + 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); +} + +////////////////////////////////////////////////////////////////////////////// +// +// 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_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_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_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); +} + +////////////////////////////////////////////////////////////////////////////// +// +// Rasterizer + +typedef struct stbtt__hheap_chunk +{ + 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; +} 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; + } +} + +static void stbtt__hheap_free(stbtt__hheap *hh, void *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; + } +} + +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 +} stbtt__active_edge; + +#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 +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 +// 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; + } +} + +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 + +// 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 + } +} + +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; + } +} + +// 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) + +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; + } +} + +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; + } + } +} + +static void stbtt__sort_edges(stbtt__edge *p, int n) +{ + stbtt__sort_edges_quicksort(p, n); + stbtt__sort_edges_ins_sort(p, n); +} + +typedef struct +{ + 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); +} + +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; +} + +// 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; +} + +// 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; +error: + 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); + } +} + +STBTT_DEF void stbtt_FreeBitmap(unsigned char *bitmap, void *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; +} + +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); +} + +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; + + 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); + + 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_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); +} + +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_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); +} + +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); +} + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// 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 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); + + 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; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// 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 + +typedef int stbrp_coord; + +//////////////////////////////////////////////////////////////////////////////////// +// // +// // +// COMPILER WARNING ?!?!? // +// // +// // +// if you get a compile warning due to these symbols being defined more than // +// once, move #include "stb_rect_pack.h" before #include "stb_truetype.h" // +// // +//////////////////////////////////////////////////////////////////////////////////// + +typedef struct +{ + int width,height; + int x,y,bottom_y; +} stbrp_context; + +typedef struct +{ + unsigned char x; +} stbrp_node; + +struct stbrp_rect +{ + 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); +} + +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 + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-AWESOME (tm Ryan Gordon) packing using stb_rect_pack.h. If +// stb_rect_pack.h isn't available, it uses the BakeFontBitmap strategy. + +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); + + 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; + + stbrp_init_target(context, pw-padding, ph-padding, nodes, num_nodes); + + if (pixels) + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + + return 1; +} + +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_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; +} + +#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; + } +} + +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; + } +} + +static float stbtt__oversample_shift(int oversample) +{ + 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); +} + +// 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; +} + +// 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; +} + +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); +} + +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; + + // 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; + + 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)); + + 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_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) +{ + 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; +} + + +////////////////////////////////////////////////////////////////////////////// +// +// 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); +} + +// 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; +} + +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; +} + +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_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; + } +} + +#endif // STB_TRUETYPE_IMPLEMENTATION + + +// FULL VERSION HISTORY +// +// 1.10 (2016-04-02) allow user-defined fabs() replacement +// fix memory leak if fontsize=0.0 +// fix warning from duplicate typedef +// 1.09 (2016-01-16) warning fix; avoid crash on outofmem; use alloc userdata for PackFontRanges +// 1.08 (2015-09-13) document stbtt_Rasterize(); fixes for vertical & horizontal edges +// 1.07 (2015-08-01) allow PackFontRanges to accept arrays of sparse codepoints; +// allow PackFontRanges to pack and render in separate phases; +// fix stbtt_GetFontOFfsetForIndex (never worked for non-0 input?); +// fixed an assert() bug in the new rasterizer +// replace assert() with STBTT_assert() in new rasterizer +// 1.06 (2015-07-14) performance improvements (~35% faster on x86 and x64 on test machine) +// also more precise AA rasterizer, except if shapes overlap +// remove need for STBTT_sort +// 1.05 (2015-04-15) fix misplaced definitions for STBTT_STATIC +// 1.04 (2015-04-15) typo in example +// 1.03 (2015-04-12) STBTT_STATIC, fix memory leak in new packing, various fixes +// 1.02 (2014-12-10) fix various warnings & compile issues w/ stb_rect_pack, C++ +// 1.01 (2014-12-08) fix subpixel position when oversampling to exactly match +// non-oversampled; STBTT_POINT_SIZE for packed case only +// 1.00 (2014-12-06) add new PackBegin etc. API, w/ support for oversampling +// 0.99 (2014-09-18) fix multiple bugs with subpixel rendering (ryg) +// 0.9 (2014-08-07) support certain mac/iOS fonts without an MS platformID +// 0.8b (2014-07-07) fix a warning +// 0.8 (2014-05-25) fix a few more warnings +// 0.7 (2013-09-25) bugfix: subpixel glyph bug fixed in 0.5 had come back +// 0.6c (2012-07-24) improve documentation +// 0.6b (2012-07-20) fix a few more warnings +// 0.6 (2012-07-17) fix warnings; added stbtt_ScaleForMappingEmToPixels, +// stbtt_GetFontBoundingBox, stbtt_IsGlyphEmpty +// 0.5 (2011-12-09) bugfixes: +// subpixel glyph renderer computed wrong bounding box +// first vertex of shape can be off-curve (FreeSans) +// 0.4b (2011-12-03) fixed an error in the font baking example +// 0.4 (2011-12-01) kerning, subpixel rendering (tor) +// bugfixes for: +// codepoint-to-glyph conversion using table fmt=12 +// codepoint-to-glyph conversion using table fmt=4 +// stbtt_GetBakedQuad with non-square texture (Zer) +// updated Hello World! sample to use kerning and subpixel +// fixed some warnings +// 0.3 (2009-06-24) cmap fmt=12, compound shapes (MM) +// userdata, malloc-from-userdata, non-zero fill (stb) +// 0.2 (2009-03-11) Fix unsigned/signed char warnings +// 0.1 (2009-03-09) First public release +// diff --git a/attachments/simple_engine/imgui_system.cpp b/attachments/simple_engine/imgui_system.cpp new file mode 100644 index 00000000..e3d17095 --- /dev/null +++ b/attachments/simple_engine/imgui_system.cpp @@ -0,0 +1,1051 @@ +#include "imgui_system.h" +#include "renderer.h" +#include "audio_system.h" + +// Include ImGui headers +#include "imgui/imgui.h" + +#include + +// This implementation corresponds to the GUI chapter in the tutorial: +// @see en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc + +ImGuiSystem::ImGuiSystem() { + // Constructor implementation +} + +ImGuiSystem::~ImGuiSystem() { + // Destructor implementation + Cleanup(); +} + +bool ImGuiSystem::Initialize(Renderer* renderer, uint32_t width, uint32_t height) { + if (initialized) { + return true; + } + + this->renderer = renderer; + this->width = width; + this->height = height; + + // Create ImGui context + context = ImGui::CreateContext(); + if (!context) { + std::cerr << "Failed to create ImGui context" << std::endl; + return false; + } + + // Configure ImGui + ImGuiIO& io = ImGui::GetIO(); + // Set display size + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); + io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + + // Set up ImGui style + ImGui::StyleColorsDark(); + + // Create Vulkan resources + if (!createResources()) { + std::cerr << "Failed to create ImGui Vulkan resources" << std::endl; + Cleanup(); + return false; + } + + // Initialize per-frame buffers containers + if (renderer) { + uint32_t frames = renderer->GetMaxFramesInFlight(); + vertexBuffers.clear(); vertexBuffers.reserve(frames); + vertexBufferMemories.clear(); vertexBufferMemories.reserve(frames); + indexBuffers.clear(); indexBuffers.reserve(frames); + indexBufferMemories.clear(); indexBufferMemories.reserve(frames); + for (uint32_t i = 0; i < frames; ++i) { + vertexBuffers.emplace_back(nullptr); + vertexBufferMemories.emplace_back(nullptr); + indexBuffers.emplace_back(nullptr); + indexBufferMemories.emplace_back(nullptr); + } + vertexCounts.assign(frames, 0); + indexCounts.assign(frames, 0); + } + + initialized = true; + return true; +} + +void ImGuiSystem::Cleanup() { + if (!initialized) { + return; + } + + // Wait for the device to be idle before cleaning up + if (renderer) { + renderer->WaitIdle(); + } + // Destroy ImGui context + if (context) { + ImGui::DestroyContext(context); + context = nullptr; + } + + initialized = false; +} + +void ImGuiSystem::SetAudioSystem(AudioSystem* audioSystem) { + this->audioSystem = audioSystem; + + // Load the grass-step-right.wav file and create audio source + if (audioSystem) { + if (audioSystem->LoadAudio("../Assets/grass-step-right.wav", "grass_step")) { + audioSource = audioSystem->CreateAudioSource("grass_step"); + if (audioSource) { + audioSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + audioSource->SetVolume(0.8f); + audioSource->SetLoop(true); + std::cout << "Audio source created and configured for HRTF demo" << std::endl; + } + } + + // Also create a debug ping source for testing + debugPingSource = audioSystem->CreateDebugPingSource("debug_ping"); + if (debugPingSource) { + debugPingSource->SetPosition(audioSourceX, audioSourceY, audioSourceZ); + debugPingSource->SetVolume(0.8f); + debugPingSource->SetLoop(true); + std::cout << "Debug ping source created for audio debugging" << std::endl; + } + } +} + +void ImGuiSystem::NewFrame() { + if (!initialized) { + return; + } + + ImGui::NewFrame(); + + // Loading overlay: show only a fullscreen progress bar while model/textures are loading + if (renderer) { + const uint32_t scheduled = renderer->GetTextureTasksScheduled(); + const uint32_t completed = renderer->GetTextureTasksCompleted(); + const bool modelLoading = renderer->IsLoading(); + if (modelLoading || (scheduled > 0 && completed < scheduled)) { + 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); + if (modelLoading) { + ImGui::Text("Loading model..."); + } else { + ImGui::Text("Loading textures: %u / %u", completed, scheduled); + } + ImGui::EndGroup(); + ImGui::PopStyleVar(); + } + ImGui::End(); + // Do not draw the rest of the UI until loading finishes + return; + } + } + + // 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 = 3.0f; // Higher default for emissive lighting + 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 = 3.0f; // Reset to higher value for emissive lighting + 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(); + + // Sun position control (punctual light in GLTF) + ImGui::Text("Sun Position in Sky:"); + if (renderer) { + float sun = renderer->GetSunPosition(); + if (ImGui::SliderFloat("Sun Position", &sun, 0.0f, 1.0f, "%.2f")) { + renderer->SetSunPosition(sun); + } + ImGui::SameLine(); + if (ImGui::Button("Noon")) { sun = 0.5f; renderer->SetSunPosition(sun); } + ImGui::SameLine(); + if (ImGui::Button("Night")) { sun = 0.0f; renderer->SetSunPosition(sun); } + ImGui::Text("Tip: 0/1 = Night, 0.5 = Noon. Warmer tint near horizon simulates evening."); + } + + 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::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; + } + + ImGuiIO& io = ImGui::GetIO(); + + // 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 +} + +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; + } + + ImGuiIO& io = ImGui::GetIO(); + io.AddInputCharacter(c); +} + +void ImGuiSystem::HandleResize(uint32_t width, uint32_t height) { + if (!initialized) { + return; + } + + this->width = width; + this->height = height; + + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); +} + +bool ImGuiSystem::WantCaptureKeyboard() const { + if (!initialized) { + return false; + } + + return ImGui::GetIO().WantCaptureKeyboard; +} + +bool ImGuiSystem::WantCaptureMouse() const { + if (!initialized) { + return false; + } + + return ImGui::GetIO().WantCaptureMouse; +} + +bool ImGuiSystem::createResources() { + // Create all Vulkan resources needed for ImGui rendering + 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::createDescriptorSetLayout() { + try { + vk::DescriptorSetLayoutBinding binding; + binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; + binding.descriptorCount = 1; + binding.stageFlags = vk::ShaderStageFlagBits::eFragment; + binding.binding = 0; + + vk::DescriptorSetLayoutCreateInfo layoutInfo; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor set layout: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createDescriptorPool() { + try { + vk::DescriptorPoolSize poolSize; + poolSize.type = vk::DescriptorType::eCombinedImageSampler; + poolSize.descriptorCount = 1; + + vk::DescriptorPoolCreateInfo poolInfo; + poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + descriptorPool = vk::raii::DescriptorPool(device, poolInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor pool: " << e.what() << std::endl; + return false; + } +} + +bool ImGuiSystem::createDescriptorSet() { + try { + vk::DescriptorSetAllocateInfo allocInfo; + allocInfo.descriptorPool = *descriptorPool; + allocInfo.descriptorSetCount = 1; + allocInfo.pSetLayouts = &(*descriptorSetLayout); + + const vk::raii::Device& device = renderer->GetRaiiDevice(); + vk::raii::DescriptorSets descriptorSets(device, allocInfo); + descriptorSet = std::move(descriptorSets[0]); // Store the first (and only) descriptor set + std::cout << "ImGui created descriptor set with handle: " << *descriptorSet << std::endl; + + // 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::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; + } +} diff --git a/attachments/simple_engine/imgui_system.h b/attachments/simple_engine/imgui_system.h new file mode 100644 index 00000000..f9e2c237 --- /dev/null +++ b/attachments/simple_engine/imgui_system.h @@ -0,0 +1,222 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Forward declarations +class Renderer; +class AudioSystem; +class AudioSource; +struct ImGuiContext; + +/** + * @brief Class for managing ImGui integration with Vulkan. + * + * 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(); + + /** + * @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); +}; diff --git a/attachments/simple_engine/install_dependencies_linux.sh b/attachments/simple_engine/install_dependencies_linux.sh new file mode 100755 index 00000000..e82daebc --- /dev/null +++ b/attachments/simple_engine/install_dependencies_linux.sh @@ -0,0 +1,153 @@ +#!/bin/bash + +# Install script for Simple Game Engine dependencies on Linux +# This script installs all required dependencies for building the Simple Game Engine + +set -e # Exit on any error + +echo "Installing Simple Game Engine dependencies for Linux..." + +# Detect the Linux distribution +if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$NAME + VER=$VERSION_ID +elif type lsb_release >/dev/null 2>&1; then + OS=$(lsb_release -si) + VER=$(lsb_release -sr) +elif [ -f /etc/lsb-release ]; then + . /etc/lsb-release + OS=$DISTRIB_ID + VER=$DISTRIB_RELEASE +elif [ -f /etc/debian_version ]; then + OS=Debian + VER=$(cat /etc/debian_version) +else + OS=$(uname -s) + VER=$(uname -r) +fi + +echo "Detected OS: $OS $VER" + +# Function to install dependencies on Ubuntu/Debian +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 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 \ + 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 +} + +# 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" +} + +# 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 +} + +# Install dependencies based on detected OS +case "$OS" in + "Ubuntu"* | "Debian"* | "Linux Mint"*) + install_ubuntu_debian + ;; + "Fedora"* | "Red Hat"* | "CentOS"* | "Rocky"*) + install_fedora_rhel + ;; + "Arch"* | "Manjaro"*) + install_arch + ;; + *) + echo "Unsupported Linux distribution: $OS" + echo "Please install the following dependencies manually:" + echo "- CMake (3.29 or later)" + echo "- Vulkan SDK" + echo "- GLFW3 development libraries" + echo "- GLM (OpenGL Mathematics) library" + echo "- OpenAL development libraries" + echo "- KTX library" + echo "- STB library" + echo "- tinygltf library" + echo "- Slang compiler" + exit 1 + ;; +esac + +echo "" +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 "" +echo "Or use the provided CMake build command:" +echo "cmake --build cmake-build-debug --target SimpleEngine -j 10" diff --git a/attachments/simple_engine/install_dependencies_windows.bat b/attachments/simple_engine/install_dependencies_windows.bat new file mode 100644 index 00000000..0fe383f4 --- /dev/null +++ b/attachments/simple_engine/install_dependencies_windows.bat @@ -0,0 +1,43 @@ +@echo off +REM Install script for Simple Game Engine dependencies on Windows +REM This script installs all required dependencies for building the Simple Game Engine + +echo Installing Simple Game Engine dependencies for Windows... + +:: Check if vcpkg is installed +where vcpkg >nul 2>nul +if %ERRORLEVEL% neq 0 ( + echo vcpkg not found. Please install vcpkg first. + echo Visit https://github.com/microsoft/vcpkg for installation instructions. + echo Typically, you would: + echo 1. git clone https://github.com/Microsoft/vcpkg.git + echo 2. cd vcpkg + echo 3. .\bootstrap-vcpkg.bat + echo 4. Add vcpkg to your PATH + exit /b 1 +) + +:: Enable binary caching for vcpkg +echo Enabling binary caching for vcpkg... +set VCPKG_BINARY_SOURCES=clear;files,%TEMP%\vcpkg-cache,readwrite + +:: Create cache directory if it doesn't exist +if not exist %TEMP%\vcpkg-cache mkdir %TEMP%\vcpkg-cache + +:: Install all dependencies at once using vcpkg with parallel installation +echo Installing all dependencies... +vcpkg install --triplet=x64-windows --x-manifest-root=%~dp0 --feature-flags=binarycaching,manifests --x-install-root=%VCPKG_INSTALLATION_ROOT%/installed + +:: Remind about Vulkan SDK +echo. +echo Don't forget to install the Vulkan SDK from https://vulkan.lunarg.com/ +echo. + +echo All dependencies have been installed successfully! +echo You can now use CMake to build your Vulkan project. +echo. +echo Example CMake command: +echo cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[path\to\vcpkg]\scripts\buildsystems\vcpkg.cmake +echo cmake --build build + +exit /b 0 diff --git a/attachments/simple_engine/main.cpp b/attachments/simple_engine/main.cpp new file mode 100644 index 00000000..ba6b0ddb --- /dev/null +++ b/attachments/simple_engine/main.cpp @@ -0,0 +1,102 @@ +#include "engine.h" +#include "transform_component.h" +#include "camera_component.h" +#include "scene_loading.h" + +#include +#include +#include + +// Constants +constexpr int WINDOW_WIDTH = 800; +constexpr int WINDOW_HEIGHT = 600; +#if defined(NDEBUG) +constexpr bool ENABLE_VALIDATION_LAYERS = false; +#else +constexpr bool ENABLE_VALIDATION_LAYERS = true; +#endif + + +/** + * @brief Set up a simple scene with a camera and some objects. + * @param engine The engine to set up the scene in. + */ +void SetupScene(Engine* engine) { + // Create a camera entity + Entity* cameraEntity = engine->CreateEntity("Camera"); + if (!cameraEntity) { + throw std::runtime_error("Failed to create camera entity"); + } + + // Add a transform component to the camera + auto* cameraTransform = cameraEntity->AddComponent(); + cameraTransform->SetPosition(glm::vec3(0.0f, 0.0f, 3.0f)); + + // Add a camera component to the camera entity + auto* camera = cameraEntity->AddComponent(); + camera->SetAspectRatio(static_cast(WINDOW_WIDTH) / static_cast(WINDOW_HEIGHT)); + + // Set the camera as the active camera + engine->SetActiveCamera(camera); + + // Kick off GLTF model loading on a background thread so the main loop can start and render the UI/progress bar + if (auto* renderer = engine->GetRenderer()) { + renderer->SetLoading(true); + } + std::thread([engine]{ + LoadGLTFModel(engine, "../Assets/bistro/bistro.gltf"); + }).detach(); +} + +#if defined(PLATFORM_ANDROID) +/** + * @brief Android entry point. + * @param app The Android app. + */ +void android_main(android_app* app) { + try { + // Create the engine + Engine engine; + + // 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; + } +} +#endif diff --git a/attachments/simple_engine/memory_pool.cpp b/attachments/simple_engine/memory_pool.cpp new file mode 100644 index 00000000..2e2fbae1 --- /dev/null +++ b/attachments/simple_engine/memory_pool.cpp @@ -0,0 +1,511 @@ +#include "memory_pool.h" +#include +#include +#include + +MemoryPool::MemoryPool(const vk::raii::Device& device, const vk::raii::PhysicalDevice& physicalDevice) + : device(device), physicalDevice(physicalDevice) { +} + + +MemoryPool::~MemoryPool() { + // RAII will handle cleanup automatically + std::lock_guard lock(poolMutex); + pools.clear(); +} + +bool MemoryPool::initialize() { + std::lock_guard lock(poolMutex); + + try { + // Configure default pool settings based on typical usage patterns + + // Vertex buffer pool: Large allocations, device-local (increased for large models like bistro) + configurePool( + PoolType::VERTEX_BUFFER, + 128 * 1024 * 1024, // 128MB blocks (doubled) + 4096, // 4KB allocation units + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + // Index buffer pool: Medium allocations, device-local (increased for large models like bistro) + configurePool( + PoolType::INDEX_BUFFER, + 64 * 1024 * 1024, // 64MB blocks (doubled) + 2048, // 2KB allocation units + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + // Uniform buffer pool: Small allocations, host-visible + // Use 64-byte alignment to match nonCoherentAtomSize and prevent validation errors + configurePool( + PoolType::UNIFORM_BUFFER, + 4 * 1024 * 1024, // 4MB blocks + 64, // 64B allocation units (aligned to nonCoherentAtomSize) + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Staging buffer pool: Variable allocations, host-visible + // Use 64-byte alignment to match nonCoherentAtomSize and prevent validation errors + configurePool( + PoolType::STAGING_BUFFER, + 16 * 1024 * 1024, // 16MB blocks + 64, // 64B allocation units (aligned to nonCoherentAtomSize) + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Texture image pool: 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; + } +} + +void MemoryPool::configurePool( + const PoolType poolType, + const vk::DeviceSize blockSize, + const vk::DeviceSize allocationUnit, + const vk::MemoryPropertyFlags properties) { + + PoolConfig config; + config.blockSize = blockSize; + config.allocationUnit = allocationUnit; + config.properties = properties; + + poolConfigs[poolType] = config; +} + +uint32_t MemoryPool::findMemoryType(const uint32_t typeFilter, const vk::MemoryPropertyFlags properties) const { + const vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && + (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + + throw std::runtime_error("Failed to find suitable memory type"); +} + +std::unique_ptr MemoryPool::createMemoryBlock(PoolType poolType, vk::DeviceSize size) { + 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::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::pair MemoryPool::findSuitableBlock(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + poolIt = pools.try_emplace( poolType ).first; + } + + auto& poolBlocks = poolIt->second; + const PoolConfig& config = poolConfigs[poolType]; + + // Calculate required units (accounting for size alignment) + const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; + const size_t requiredUnits = static_cast((alignedSize + config.allocationUnit - 1) / config.allocationUnit); + + // Search existing blocks for sufficient free space with proper offset alignment + for (const auto& block : poolBlocks) { + const vk::DeviceSize unit = config.allocationUnit; + const size_t totalUnits = block->freeList.size(); + + size_t i = 0; + while (i < totalUnits) { + // Ensure starting unit produces an offset aligned to 'alignment' + vk::DeviceSize startOffset = static_cast(i) * unit; + if ((alignment > 0) && (startOffset % alignment != 0)) { + // Advance i to the next unit that aligns with 'alignment' + const vk::DeviceSize remainder = startOffset % alignment; + const vk::DeviceSize advanceBytes = alignment - remainder; + const size_t advanceUnits = static_cast((advanceBytes + unit - 1) / unit); + i += std::max(advanceUnits, 1); + continue; + } + + // From aligned i, check for consecutive free units + size_t consecutiveFree = 0; + size_t j = i; + while (j < totalUnits && block->freeList[j] && consecutiveFree < requiredUnits) { + ++consecutiveFree; + ++j; + } + + if (consecutiveFree >= requiredUnits) { + return {block.get(), i}; + } + + // Move past the checked range + i = (j > i) ? j : (i + 1); + } + } + + // No suitable block found; create a new one on demand (no hard limits, allowed during rendering) + try { + auto newBlock = createMemoryBlock(poolType, alignedSize); + poolBlocks.push_back(std::move(newBlock)); + std::cout << "Created new memory block (pool type: " + << static_cast(poolType) << ")" << std::endl; + return {poolBlocks.back().get(), 0}; + } catch (const std::exception& e) { + std::cerr << "Failed to create new memory block: " << e.what() << std::endl; + return {nullptr, 0}; + } +} + +std::unique_ptr MemoryPool::allocate(PoolType poolType, vk::DeviceSize size, vk::DeviceSize alignment) { + std::lock_guard lock(poolMutex); + + auto [block, startUnit] = findSuitableBlock(poolType, size, alignment); + if (!block) { + return nullptr; + } + + const PoolConfig& config = poolConfigs[poolType]; + + // Calculate required units (accounting for alignment) + const vk::DeviceSize alignedSize = ((size + alignment - 1) / alignment) * alignment; + const size_t requiredUnits = (alignedSize + config.allocationUnit - 1) / config.allocationUnit; + + // Mark units as used + for (size_t i = startUnit; i < startUnit + requiredUnits; ++i) { + block->freeList[i] = false; + } + + // Create allocation info + auto allocation = std::make_unique(); + allocation->memory = *block->memory; + allocation->offset = startUnit * config.allocationUnit; + allocation->size = alignedSize; + allocation->memoryTypeIndex = block->memoryTypeIndex; + allocation->isMapped = block->isMapped; + allocation->mappedPtr = block->isMapped ? + static_cast(block->mappedPtr) + allocation->offset : nullptr; + + block->used += alignedSize; + + return allocation; +} + +void MemoryPool::deallocate(std::unique_ptr allocation) { + if (!allocation) { + return; + } + + std::lock_guard lock(poolMutex); + + // Find the block that contains this allocation + for (auto& [poolType, poolBlocks] : pools) { + const PoolConfig& config = poolConfigs[poolType]; + + for (auto& block : poolBlocks) { + if (*block->memory == allocation->memory) { + // Calculate which units to free + size_t startUnit = allocation->offset / config.allocationUnit; + size_t numUnits = (allocation->size + config.allocationUnit - 1) / config.allocationUnit; + + // Mark units as free + for (size_t i = startUnit; i < startUnit + numUnits; ++i) { + 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)}; +} + +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)}; +} + +std::pair MemoryPool::getMemoryUsage(PoolType poolType) const { + std::lock_guard lock(poolMutex); + + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + return {0, 0}; + } + + vk::DeviceSize used = 0; + vk::DeviceSize total = 0; + + for (const auto& block : poolIt->second) { + used += block->used; + total += block->size; + } + + return {used, total}; +} + +std::pair MemoryPool::getTotalMemoryUsage() const { + std::lock_guard lock(poolMutex); + + vk::DeviceSize totalUsed = 0; + vk::DeviceSize totalAllocated = 0; + + for (const auto& [poolType, poolBlocks] : pools) { + for (const auto& block : poolBlocks) { + totalUsed += block->used; + totalAllocated += block->size; + } + } + + return {totalUsed, totalAllocated}; +} + +bool MemoryPool::preAllocatePools() { + std::lock_guard lock(poolMutex); + + try { + std::cout << "Pre-allocating initial memory blocks for pools..." << std::endl; + + // Pre-allocate at least one block for each pool type + for (const auto& [poolType, config] : poolConfigs) { + auto poolIt = pools.find(poolType); + if (poolIt == pools.end()) { + poolIt = pools.try_emplace( poolType ).first; + } + + auto& poolBlocks = poolIt->second; + if (poolBlocks.empty()) { + // Create initial block for this pool type + auto newBlock = createMemoryBlock(poolType, config.blockSize); + poolBlocks.push_back(std::move(newBlock)); + std::cout << " Pre-allocated block for pool type " << static_cast(poolType) << std::endl; + } + } + + std::cout << "Memory pool pre-allocation completed successfully" << std::endl; + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to pre-allocate memory pools: " << e.what() << std::endl; + return false; + } +} + +void MemoryPool::setRenderingActive(bool active) { + std::lock_guard lock(poolMutex); + renderingActive = active; +} + +bool MemoryPool::isRenderingActive() const { + std::lock_guard lock(poolMutex); + return renderingActive; +} diff --git a/attachments/simple_engine/memory_pool.h b/attachments/simple_engine/memory_pool.h new file mode 100644 index 00000000..c4deeec2 --- /dev/null +++ b/attachments/simple_engine/memory_pool.h @@ -0,0 +1,197 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief Memory pool allocator for Vulkan resources + * + * This class implements a memory pool system to reduce memory fragmentation + * and improve allocation performance by pre-allocating large chunks of memory + * and sub-allocating from them. + */ +class MemoryPool { +public: + /** + * @brief Types of memory pools based on usage patterns + */ + enum class PoolType { + VERTEX_BUFFER, // Device-local memory for vertex data + INDEX_BUFFER, // Device-local memory for index data + UNIFORM_BUFFER, // Host-visible memory for uniform data + STAGING_BUFFER, // Host-visible memory for staging operations + TEXTURE_IMAGE // Device-local memory for texture images + }; + + /** + * @brief Allocation information for a memory block + */ + struct Allocation { + vk::DeviceMemory memory; // The underlying device memory + vk::DeviceSize offset; // Offset within the memory block + vk::DeviceSize size; // Size of the allocation + uint32_t memoryTypeIndex; // Memory type index + bool isMapped; // Whether the memory is persistently mapped + void* mappedPtr; // Mapped pointer (if applicable) + }; + + /** + * @brief Memory block within a pool + */ + struct MemoryBlock { + vk::raii::DeviceMemory memory; // RAII wrapper for device memory + vk::DeviceSize size; // Total size of the block + vk::DeviceSize used; // Currently used bytes + uint32_t memoryTypeIndex; // Memory type index + bool isMapped; // Whether the block is mapped + void* mappedPtr; // Mapped pointer (if applicable) + std::vector freeList; // Free list for sub-allocations + vk::DeviceSize allocationUnit; // Size of each allocation unit + }; + +private: + const vk::raii::Device& device; + const vk::raii::PhysicalDevice& physicalDevice; + vk::PhysicalDeviceMemoryProperties memPropsCache{}; + + + // Pool configurations + struct PoolConfig { + vk::DeviceSize blockSize; // Size of each memory block + vk::DeviceSize allocationUnit; // Minimum allocation unit + vk::MemoryPropertyFlags properties; // Memory properties + }; + + // Memory pools for different types + std::unordered_map>> pools; + std::unordered_map poolConfigs; + + // Thread safety + mutable std::mutex poolMutex; + + // Optional rendering state flag (no allocation restrictions enforced) + bool renderingActive = false; + + // Helper methods + uint32_t findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const; + std::unique_ptr createMemoryBlock(PoolType poolType, vk::DeviceSize size); + // 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; +}; diff --git a/attachments/simple_engine/mesh_component.cpp b/attachments/simple_engine/mesh_component.cpp new file mode 100644 index 00000000..6e2eef5d --- /dev/null +++ b/attachments/simple_engine/mesh_component.cpp @@ -0,0 +1,86 @@ +#include "mesh_component.h" +#include "model_loader.h" +#include + +// 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) { + auto theta = static_cast(lat * M_PI / segments); // Latitude angle (0 to PI) + float sinTheta = sinf(theta); + float cosTheta = cosf(theta); + + for (int lon = 0; lon <= segments; ++lon) { + auto phi = static_cast(lon * 2.0 * M_PI / segments); // Longitude angle (0 to 2*PI) + float sinPhi = sinf(phi); + 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 + glm::vec2 texCoord = { + static_cast(lon) / static_cast(segments), + static_cast(lat) / static_cast(segments) + }; + + // Calculate tangent (derivative with respect to longitude) + glm::vec3 tangent = { + -sinTheta * sinPhi, + 0.0f, + sinTheta * cosPhi + }; + 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) { + int current = lat * (segments + 1) + lon; + 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; + } + + // Copy vertex and index data from the model + vertices = model->GetVertices(); + indices = model->GetIndices(); + + RecomputeLocalAABB(); +} diff --git a/attachments/simple_engine/mesh_component.h b/attachments/simple_engine/mesh_component.h new file mode 100644 index 00000000..00d382b4 --- /dev/null +++ b/attachments/simple_engine/mesh_component.h @@ -0,0 +1,461 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "component.h" + +/** + * @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() { + 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); + 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); + 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); + 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() { + vk::VertexInputBindingDescription bindingDescription( + 0, // binding + sizeof(Vertex), // stride + vk::VertexInputRate::eVertex // inputRate + ); + return bindingDescription; + } + + static std::array getAttributeDescriptions() { + 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 : 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/model_loader.cpp b/attachments/simple_engine/model_loader.cpp new file mode 100644 index 00000000..90b09076 --- /dev/null +++ b/attachments/simple_engine/model_loader.cpp @@ -0,0 +1,1615 @@ +#include "model_loader.h" +#include "renderer.h" +#include "mesh_component.h" +#include +#include +#include +#include +#include + +// 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; +} + +// 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(); +} + +bool ModelLoader::Initialize(Renderer* _renderer) { + renderer = _renderer; + + if (!renderer) { + std::cerr << "ModelLoader::Initialize: Renderer is null" << std::endl; + return false; + } + + 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::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; + } + + // 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; + } + light_scale = EMISSIVE_SCALE_FACTOR; + + // Track loaded textures to prevent loading the same texture multiple times + std::set loadedTextures; + + // 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] + ); + } + + // Parse KHR_materials_emissive_strength extension + auto extensionIt = gltfMaterial.extensions.find("KHR_materials_emissive_strength"); + if (extensionIt != gltfMaterial.extensions.end()) { + const tinygltf::Value& extension = extensionIt->second; + if (extension.Has("emissiveStrength") && extension.Get("emissiveStrength").IsNumber()) { + material->emissiveStrength = static_cast(extension.Get("emissiveStrength").Get()) * light_scale; + } + } else { + // Default emissive strength is 1.0, according to GLTF spec, scaled for engine units + material->emissiveStrength = 1.0f * light_scale; + } + + // 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()); + } + } + + // 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()) { + renderer->LoadTextureFromMemoryAsync(textureId, image.image.data(), image.width, image.height, image.component); + material->specGlossTexturePath = textureId; + material->metallicRoughnessTexturePath = textureId; // reuse binding 2 + } else if (!image.uri.empty()) { + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c)) { + renderer->LoadTextureFromMemoryAsync(textureId, data.data(), w, h, c); + 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); + 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); + 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; + // Try loading from a KTX2 file on disk first + std::vector data; int w=0,h=0,c=0; + if (LoadKTX2FileToRGBA(texIdOrPath, data, w, h, c)) { + renderer->LoadTextureFromMemoryAsync(texIdOrPath, data.data(), w, h, c); + mat->albedoTexturePath = texIdOrPath; + std::cout << " Scheduled base color KTX2 file upload (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); + 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)) { + // Load KTX2 (or KTX) file via libktx then upload from memory + std::vector data; int w=0,h=0,c=0; + if (LoadKTX2FileToRGBA(cand, data, w, h, c)) { + renderer->LoadTextureFromMemoryAsync(cand, data.data(), w, h, c); + mat->albedoTexturePath = cand; + std::cout << " Scheduled derived base color upload from normal sibling: " << cand << std::endl; + break; + } + } + } + } + } + + // Secondary heuristic: scan glTF images for base color by material-name match when still missing + for (auto &entry : materials) { + Material* mat = entry.second.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 = entry.first; + 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& attr : primitive.attributes) { + hash += "_" + attr.first + "_" + std::to_string(attr.second); + } + + 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 + 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]; + + // Handle different index types with proper vertex offset adjustment + if (indexAccessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + const auto* buf = static_cast(indexData); + for (size_t i = 0; i < indexAccessor.count; ++i) { + // FIXED: Add vertex offset to prevent index sharing between primitives + 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) { + // FIXED: Add vertex offset to prevent index sharing between primitives + materialMesh.indices.push_back(buf[i] + vertexOffsetInMaterialMesh); + } + } + } + + // Get vertex positions + auto posIt = primitive.attributes.find("POSITION"); + if (posIt == primitive.attributes.end()) { + std::cerr << "No POSITION attribute found in primitive" << std::endl; + continue; + } + + const tinygltf::Accessor& posAccessor = gltfModel.accessors[posIt->second]; + const tinygltf::BufferView& posBufferView = gltfModel.bufferViews[posAccessor.bufferView]; + const tinygltf::Buffer& posBuffer = gltfModel.buffers[posBufferView.buffer]; + + const auto* positions = reinterpret_cast( + &posBuffer.data[posBufferView.byteOffset + posAccessor.byteOffset]); + + // Get texture coordinates (if available) + const float* texCoords = nullptr; + 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]; + const tinygltf::Buffer& texCoordBuffer = gltfModel.buffers[texCoordBufferView.buffer]; + texCoords = reinterpret_cast( + &texCoordBuffer.data[texCoordBufferView.byteOffset + texCoordAccessor.byteOffset]); + } + + // Get normals (if available) + const float* normals = nullptr; + 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]; + const tinygltf::Buffer& normalBuffer = gltfModel.buffers[normalBufferView.buffer]; + normals = reinterpret_cast( + &normalBuffer.data[normalBufferView.byteOffset + normalAccessor.byteOffset]); + } + + // Create vertices in their original coordinate system (no transformation applied here) + for (size_t i = 0; i < posAccessor.count; ++i) { + Vertex vertex{}; + + // Position (keep in an original coordinate system) + vertex.position = glm::vec3( + positions[i * 3 + 0], + positions[i * 3 + 1], + positions[i * 3 + 2] + ); + + // Normal (keep in an original coordinate system) + if (normals) { + vertex.normal = glm::vec3( + normals[i * 3 + 0], + normals[i * 3 + 1], + normals[i * 3 + 2] + ); + } else { + vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f); // Default forward normal + } + + // Texture coordinates + if (texCoords) { + vertex.texCoord = glm::vec2( + texCoords[i * 2 + 0], + texCoords[i * 2 + 1] + ); + } else { + vertex.texCoord = glm::vec2(0.0f, 0.0f); + } + + // Tangent (default right tangent for now, could be extracted from GLTF if available) + vertex.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + + materialMesh.vertices.push_back(vertex); + } + } // End of isFirstTimeProcessing block + + // 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); + 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: load KTX2 from the file path and upload into memory + std::vector data; int w=0,h=0,c=0; + if (LoadKTX2FileToRGBA(textureId, data, w, h, c) && + renderer->LoadTextureFromMemory(textureId, data.data(), w, h, c)) { + materialMesh.baseColorTexturePath = textureId; + materialMesh.texturePath = textureId; + std::cout << " Loaded baseColor KTX2 from file (heuristic): " << textureId << std::endl; + } else { + std::cerr << " Warning: Heuristic baseColor image has no decoded bytes and KTX2 fallback failed: " << imageUri << 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: load KTX2 from a file and upload to memory + std::vector data; int w=0,h=0,c=0; + std::string filePath = baseTexturePath + image.uri; + if (LoadKTX2FileToRGBA(filePath, data, w, h, c)) { + renderer->LoadTextureFromMemoryAsync(textureId, data.data(), w, h, c); + materialMesh.normalTexturePath = textureId; + std::cout << " Scheduled normal KTX2 upload: " << filePath << std::endl; + } else { + std::cerr << " Failed to decode normal KTX2 file: " << filePath << 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 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 = material->emissiveStrength; + 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 = material->emissiveStrength; + 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 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; +} + +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 new file mode 100644 index 00000000..56787d9b --- /dev/null +++ b/attachments/simple_engine/model_loader.h @@ -0,0 +1,280 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "mesh_component.h" + +class Renderer; +class Mesh; +class Material; + +// Forward declaration for tinygltf +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 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; + + 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) +}; + +/** + * @brief Structure representing camera data extracted from GLTF. + */ +struct CameraData { + std::string name; + bool isPerspective = true; + + // Perspective camera properties + float fov = 0.785398f; // 45 degrees in radians + float aspectRatio = 1.0f; + + // Orthographic camera properties + float orthographicSize = 1.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 +}; + +/** + * @brief Structure representing mesh data for a specific material. + */ +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; + } +}; + +/** + * @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; } + +public: + // Public access to cameras for model loader + std::vector cameras; + +private: + std::string name; + std::vector vertices; + std::vector indices; + // Other model data (meshes, materials, etc.) +}; + +/** + * @brief Class for loading and managing 3D models. + */ +class ModelLoader { +public: + /** + * @brief Default constructor. + */ + ModelLoader() = default; + + /** + * @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; + + 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 new file mode 100644 index 00000000..fd3481b3 --- /dev/null +++ b/attachments/simple_engine/physics_system.cpp @@ -0,0 +1,1362 @@ +#include "physics_system.h" +#include "entity.h" +#include "renderer.h" +#include "transform_component.h" +#include "mesh_component.h" +#include + +#include +#include +#include +#include +#include +#include + + +// Concrete implementation of RigidBody +class ConcreteRigidBody : 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(); +} + +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; + 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 + if (initialized && gpuAccelerationEnabled && renderer && rigidBodies.size() <= maxGPUObjects) { + // 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}); +} + +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 + rigidBodies.push_back(std::move(rigidBody)); + + return rigidBodies.back().get(); +} + +bool PhysicsSystem::RemoveRigidBody(RigidBody* rigidBody) { + // 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); + + return true; + } + + std::cerr << "PhysicsSystem::RemoveRigidBody: Rigid body not found" << std::endl; + return false; +} + +void PhysicsSystem::SetGravity(const glm::vec3& _gravity) { + gravity = _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; + + // 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; +} + +// 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()); + + 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; + } +} + +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::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::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 new file mode 100644 index 00000000..96e5bb0a --- /dev/null +++ b/attachments/simple_engine/physics_system.h @@ -0,0 +1,394 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class Entity; +class Renderer; + +/** + * @brief Enum for different collision shapes. + */ +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; +}; + +/** + * @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) +}; + +/** + * @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 +}; + +/** + * @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) +}; + +/** + * @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 +}; + +/** + * @brief Class for managing physics simulation. + * + * This class implements the physics system as described in the Subsystems chapter: + * @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; + + /** + * @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 + 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 new file mode 100644 index 00000000..ac44c3b3 --- /dev/null +++ b/attachments/simple_engine/pipeline.cpp @@ -0,0 +1,741 @@ +#include "pipeline.h" +#include "mesh_component.h" +#include +#include + +// Constructor +Pipeline::Pipeline(VulkanDevice& device, SwapChain& swapChain) + : device(device), swapChain(swapChain) { +} + +// Destructor +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; + } +} + +// 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; + } +} + +// 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; + } +} + +// 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; + } +} + +// 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; + } +} + +// Push material properties +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()) + }; + + 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); + + 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(), fileSize); + file.close(); + + return buffer; +} diff --git a/attachments/simple_engine/pipeline.h b/attachments/simple_engine/pipeline.h new file mode 100644 index 00000000..b4ad8f6d --- /dev/null +++ b/attachments/simple_engine/pipeline.h @@ -0,0 +1,183 @@ +#pragma once + +#include +#include +#include +#define GLM_FORCE_RADIANS +#include +#include + +#include "vulkan_device.h" +#include "swap_chain.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 +}; + +/** + * @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); +}; diff --git a/attachments/simple_engine/platform.cpp b/attachments/simple_engine/platform.cpp new file mode 100644 index 00000000..29302ecd --- /dev/null +++ b/attachments/simple_engine/platform.cpp @@ -0,0 +1,512 @@ +#include "platform.h" + +#include + +#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; + } + }; +} + +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(); + + // Set up power-saving mode based on battery level + SetupPowerSavingMode(); + + // Initialize touch input handling + InitializeTouchInput(); + + return true; + } + return false; +} + +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::HasWindowResized() { + bool resized = windowResized; + windowResized = false; + return resized; +} + +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; + + if (vkCreateAndroidSurfaceKHR(instance, &createInfo, nullptr, surface) != VK_SUCCESS) { + return false; + } + + return true; +} + +void AndroidPlatform::SetResizeCallback(std::function callback) { + resizeCallback = 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::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::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::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; + + // 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 +} + +#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; +} + +void DesktopPlatform::Cleanup() { + if (window) { + glfwDestroyWindow(window); + window = nullptr; + } + + glfwTerminate(); +} + +bool DesktopPlatform::ProcessEvents() { + // Process GLFW events + glfwPollEvents(); + + // Check if the window should close + return !glfwWindowShouldClose(window); +} + +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; + } + + return true; +} + +void DesktopPlatform::SetResizeCallback(std::function callback) { + resizeCallback = 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::SetCharCallback(std::function callback) { + charCallback = std::move(callback); +} + +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::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::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); + } +} + +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); + } +} +#endif diff --git a/attachments/simple_engine/platform.h b/attachments/simple_engine/platform.h new file mode 100644 index 00000000..39f7b145 --- /dev/null +++ b/attachments/simple_engine/platform.h @@ -0,0 +1,459 @@ +#pragma once + +#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__)) +#else +#define GLFW_INCLUDE_VULKAN +#include +#define LOGI(...) printf(__VA_ARGS__); printf("\n") +#define LOGW(...) printf(__VA_ARGS__); printf("\n") +#define LOGE(...) fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n") +#endif + +/** + * @brief Interface for platform-specific functionality. + * + * 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; +}; + +#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; } +}; +#else +/** + * @brief Desktop implementation of the Platform interface. + */ +class DesktopPlatform : 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 + +/** + * @brief Factory function for creating a platform instance. + * @param args Arguments to pass to the platform constructor. + * @return A unique pointer to the platform instance. + */ +template +std::unique_ptr CreatePlatform(Args&&... args) { +#if defined(PLATFORM_ANDROID) + return std::make_unique(std::forward(args)...); +#else + return std::make_unique(); +#endif +} diff --git a/attachments/simple_engine/renderdoc_debug_system.cpp b/attachments/simple_engine/renderdoc_debug_system.cpp new file mode 100644 index 00000000..9cd23ad9 --- /dev/null +++ b/attachments/simple_engine/renderdoc_debug_system.cpp @@ -0,0 +1,121 @@ +#include "renderdoc_debug_system.h" + +#include +#include + +#if defined(_WIN32) + #define WIN32_LEAN_AND_MEAN + #include +#elif defined(__APPLE__) || defined(__linux__) + #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 +#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; +}; + +bool RenderDocDebugSystem::LoadRenderDocAPI() { + if (renderdocAvailable) return true; + + // 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")); +#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")); +#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; +} + +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"); + } +} + +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 new file mode 100644 index 00000000..e63b5bb0 --- /dev/null +++ b/attachments/simple_engine/renderdoc_debug_system.h @@ -0,0 +1,54 @@ +#pragma once + +#include "debug_system.h" + +// RenderDoc integration is optional and loaded at runtime. +// 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 : 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(); + + // 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(); + + // 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); + +private: + RenderDocDebugSystem() = default; + ~RenderDocDebugSystem() override = default; + + 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**); + + // 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; + + bool renderdocAvailable = false; +}; diff --git a/attachments/simple_engine/renderer.h b/attachments/simple_engine/renderer.h new file mode 100644 index 00000000..78c52f9e --- /dev/null +++ b/attachments/simple_engine/renderer.h @@ -0,0 +1,771 @@ +#pragma once + +#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 "memory_pool.h" +#include "model_loader.h" +#include "thread_pool.h" + +// Forward declarations +class ImGuiSystem; + +/** + * @brief Structure for Vulkan queue family indices. + */ +struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + std::optional computeFamily; + std::optional transferFamily; // optional dedicated transfer queue family + + [[nodiscard]] bool isComplete() const { + return graphicsFamily.has_value() && presentFamily.has_value() && computeFamily.has_value(); + } +}; + +/** + * @brief Structure for swap chain support details. + */ +struct SwapChainSupportDetails { + vk::SurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; + +/** + * @brief Structure for individual light data in the storage buffer. + */ +struct LightData { + alignas(16) glm::vec4 position; // Light position (w component used for direction vs position) + alignas(16) glm::vec4 color; // Light color and intensity + alignas(16) glm::mat4 lightSpaceMatrix; // Light space matrix for shadow mapping + alignas(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; // Number of active lights (dynamic) + alignas(4) int padding0; // Padding for alignment (shadows removed) + alignas(4) float padding1; // Padding for alignment + alignas(4) float padding2; // Padding for alignment + + // Additional padding to ensure the structure size is aligned to 64 bytes (device nonCoherentAtomSize) + // Adjusted padding to maintain 256 bytes total size + alignas(4) float padding3[2]; // Add remaining bytes to reach 256 bytes total +}; + + +/** + * @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 +}; + +/** + * @brief Class for managing Vulkan rendering. + * + * This class implements the rendering pipeline as described in the Engine_Architecture chapter: + * @see en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc + */ +class Renderer { +public: + /** + * @brief Constructor with a platform. + * @param platform The platform to use for rendering. + */ + explicit Renderer(Platform* platform); + + /** + * @brief Destructor for proper cleanup. + */ + ~Renderer(); + + /** + * @brief Initialize the renderer. + * @param appName The name of the application. + * @param enableValidationLayers Whether to enable validation layers. + * @return True if initialization was successful, false otherwise. + */ + bool Initialize(const std::string& appName, bool enableValidationLayers = true); + + /** + * @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 Set sun position slider value in [0,1]. 0 and 1 = night, 0.5 = noon. + */ + void SetSunPosition(float s) { sunPosition = std::clamp(s, 0.0f, 1.0f); } + + /** + * @brief Get sun position slider value. + */ + float GetSunPosition() const { return sunPosition; } + + + /** + * @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 uploadTimelineValue.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) + std::future LoadTextureAsync(const std::string& texturePath); + + /** + * @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); + + // Progress query for UI + uint32_t GetTextureTasksScheduled() const { return textureTasksScheduled.load(); } + uint32_t GetTextureTasksCompleted() const { return textureTasksCompleted.load(); } + + // Global loading state (model/scene) + bool IsLoading() const { return loadingFlag.load(); } + 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 = true; + } + + /** + * @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); + + // 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 = 3.0f; // HDR exposure value (higher for emissive lighting) + + // Sun control (UI-driven) + float sunPosition = 0.5f; // 0..1, extremes are night, 0.5 is noon + + // 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; + 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; + std::atomic uploadTimelineValue{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; + + // Mesh resources + struct MeshResources { + vk::raii::Buffer vertexBuffer = nullptr; + std::unique_ptr vertexBufferAllocation = nullptr; + vk::raii::Buffer indexBuffer = nullptr; + std::unique_ptr indexBufferAllocation = nullptr; + uint32_t indexCount = 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 + }; + std::unordered_map textureResources; + // 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; + + // 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 + }; + + // All device extensions (required + optional) + std::vector deviceExtensions; + + // Initialization flag + bool initialized = false; + + // Framebuffer resized flag + bool 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 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); + 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); +}; diff --git a/attachments/simple_engine/renderer_compute.cpp b/attachments/simple_engine/renderer_compute.cpp new file mode 100644 index 00000000..65256065 --- /dev/null +++ b/attachments/simple_engine/renderer_compute.cpp @@ -0,0 +1,263 @@ +#include "renderer.h" +#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; + } +} + +// 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; + } +} + +// 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}; + } +} diff --git a/attachments/simple_engine/renderer_core.cpp b/attachments/simple_engine/renderer_core.cpp new file mode 100644 index 00000000..48aaffeb --- /dev/null +++ b/attachments/simple_engine/renderer_core.cpp @@ -0,0 +1,603 @@ +#include "renderer.h" +#include +#include +#include +#include +#include +#include +#include + +VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE; // In a .cpp file + +#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; +} + +// This implementation corresponds to the Engine_Architecture chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc + +// Constructor +Renderer::Renderer(Platform* platform) + : platform(platform) { + // Initialize deviceExtensions with required extensions only + // Optional extensions will be added later after checking device support + deviceExtensions = requiredDeviceExtensions; +} + +// Destructor +Renderer::~Renderer() { + Cleanup(); +} + +// Initialize the renderer +bool Renderer::Initialize(const std::string& appName, bool enableValidationLayers) { + 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; + } + + // Create default texture resources + if (!createDefaultTextureResources()) { + std::cerr << "Failed to create default texture resources" << std::endl; + return false; + } + + // 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; +} + +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 + 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(); + } + std::cout << "Renderer cleanup completed." << std::endl; + initialized = false; + } +} + +// Create instance +bool Renderer::createInstance(const std::string& appName, bool enableValidationLayers) { + try { + // Create application info + vk::ApplicationInfo appInfo{ + .pApplicationName = appName.c_str(), + .applicationVersion = VK_MAKE_VERSION(1, 0, 0), + .pEngineName = "Simple Engine", + .engineVersion = VK_MAKE_VERSION(1, 0, 0), + .apiVersion = VK_API_VERSION_1_3 + }; + + // Get required extensions + std::vector extensions; + + // Add required extensions for GLFW +#if defined(PLATFORM_DESKTOP) + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + extensions.insert(extensions.end(), glfwExtensions, glfwExtensions + glfwExtensionCount); +#endif + + // Add debug extension if validation layers are enabled + if (enableValidationLayers) { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + // 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; + } +} + +// 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; + } +} + +// Create surface +bool Renderer::createSurface() { + try { + // Create surface + VkSurfaceKHR _surface; + if (!platform->CreateVulkanSurface(*instance, &_surface)) { + std::cerr << "Failed to create window surface" << std::endl; + return false; + } + + surface = vk::raii::SurfaceKHR(instance, _surface); + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create surface: " << e.what() << std::endl; + return false; + } +} + +// Pick a physical device +bool Renderer::pickPhysicalDevice() { + try { + // Get available physical devices + std::vector devices = instance.enumeratePhysicalDevices(); + + if (devices.empty()) { + std::cerr << "Failed to find GPUs with Vulkan support" << std::endl; + return false; + } + + // Prioritize discrete GPUs (like NVIDIA RTX 2080) over integrated GPUs (like Intel UHD Graphics) + // First, collect all suitable devices with their suitability scores + std::multimap suitableDevices; + + for (auto& _device : devices) { + // Print device properties for debugging + vk::PhysicalDeviceProperties deviceProperties = _device.getProperties(); + std::cout << "Checking device: " << deviceProperties.deviceName + << " (Type: " << vk::to_string(deviceProperties.deviceType) << ")" << std::endl; + + // 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(); + + // Check which optional extensions are supported and add them to deviceExtensions + for (const auto& optionalExt : optionalDeviceExtensions) { + for (const auto& availableExt : availableExtensions) { + if (strcmp(availableExt.extensionName, optionalExt) == 0) { + deviceExtensions.push_back(optionalExt); + std::cout << "Adding optional extension: " << optionalExt << std::endl; + break; + } + } + } + } catch (const std::exception& e) { + std::cerr << "Warning: Failed to add optional extensions: " << e.what() << std::endl; + } +} + +// Create logical device +bool Renderer::createLogicalDevice(bool enableValidationLayers) { + try { + // Create queue create info for each unique queue family + std::vector queueCreateInfos; + std::set uniqueQueueFamilies = { + queueFamilyIndices.graphicsFamily.value(), + queueFamilyIndices.presentFamily.value(), + queueFamilyIndices.computeFamily.value(), + queueFamilyIndices.transferFamily.value() + }; + + float queuePriority = 1.0f; + for (uint32_t queueFamily : uniqueQueueFamilies) { + vk::DeviceQueueCreateInfo queueCreateInfo{ + .queueFamilyIndex = queueFamily, + .queueCount = 1, + .pQueuePriorities = &queuePriority + }; + queueCreateInfos.push_back(queueCreateInfo); + } + + // Enable required features + auto features = physicalDevice.getFeatures2(); + features.features.samplerAnisotropy = 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 + vk::DeviceCreateInfo createInfo{ + .pNext = &features, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data(), + .pEnabledFeatures = nullptr // Using pNext for features + }; + + // Enable validation layers if requested + if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + } + + // 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); + uploadTimelineValue.store(0, std::memory_order_relaxed); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create logical device: " << e.what() << std::endl; + return false; + } +} + +// Check validation layer support +bool Renderer::checkValidationLayerSupport() const { + // Get available layers + std::vector availableLayers = context.enumerateInstanceLayerProperties(); + + // Check if all requested layers are available + for (const char* layerName : validationLayers) { + bool layerFound = false; + + for (const auto& layerProperties : availableLayers) { + if (strcmp(layerName, layerProperties.layerName) == 0) { + layerFound = true; + break; + } + } + + if (!layerFound) { + return false; + } + } + + return true; +} diff --git a/attachments/simple_engine/renderer_pipelines.cpp b/attachments/simple_engine/renderer_pipelines.cpp new file mode 100644 index 00000000..7924d372 --- /dev/null +++ b/attachments/simple_engine/renderer_pipelines.cpp @@ -0,0 +1,686 @@ +#include "renderer.h" +#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; + } +} + +// 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); + + 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" + }; + + 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::eBack, + .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::GraphicsPipelineCreateInfo pipelineInfo{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &mainPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .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 + }; + + 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" + }; + + 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::eBack, + .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) + }; + + // Create a pipeline layout using the PBR descriptor set layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &*pbrDescriptorSetLayout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange + }; + + pbrPipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // 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::GraphicsPipelineCreateInfo opaquePipelineInfo{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .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.colorBlendOp = vk::BlendOp::eAdd; + blendedAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne; + blendedAttachment.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + blendedAttachment.alphaBlendOp = vk::BlendOp::eAdd; + vk::PipelineColorBlendStateCreateInfo colorBlendingBlended{ + .logicOpEnable = VK_FALSE, + .logicOp = vk::LogicOp::eCopy, + .attachmentCount = 1, + .pAttachments = &blendedAttachment + }; + vk::PipelineDepthStencilStateCreateInfo depthStencilBlended = depthStencil; + depthStencilBlended.depthWriteEnable = VK_FALSE; + + vk::GraphicsPipelineCreateInfo blendedPipelineInfo{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &pbrPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .stageCount = 2, + .pStages = shaderStages, + .pVertexInputState = &vertexInputInfo, + .pInputAssemblyState = &inputAssembly, + .pViewportState = &viewportState, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = &depthStencilBlended, + .pColorBlendState = &colorBlendingBlended, + .pDynamicState = &dynamicState, + .layout = *pbrPipelineLayout, + .renderPass = nullptr, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = -1 + }; + pbrBlendGraphicsPipeline = vk::raii::Pipeline(device, nullptr, blendedPipelineInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create PBR 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::eBack, + .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::GraphicsPipelineCreateInfo pipelineInfo{ + .sType = vk::StructureType::eGraphicsPipelineCreateInfo, + .pNext = &lightingPipelineRenderingCreateInfo, + .flags = vk::PipelineCreateFlags{}, + .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 + }; + + 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); +} diff --git a/attachments/simple_engine/renderer_rendering.cpp b/attachments/simple_engine/renderer_rendering.cpp new file mode 100644 index 00000000..621c8c81 --- /dev/null +++ b/attachments/simple_engine/renderer_rendering.cpp @@ -0,0 +1,1092 @@ +#include "renderer.h" +#include "imgui_system.h" +#include "imgui/imgui.h" +#include "model_loader.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// 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, + .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; + } +} + +// Create image views +bool Renderer::createImageViews() { + try { + // 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; + } +} + +// 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; + } +} + +// Create command buffers +bool Renderer::createCommandBuffers() { + try { + // Resize command buffers vector + commandBuffers.clear(); + commandBuffers.reserve(MAX_FRAMES_IN_FLIGHT); + + // Create command buffer allocation info + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = static_cast(MAX_FRAMES_IN_FLIGHT) + }; + + // Allocate command buffers + commandBuffers = vk::raii::CommandBuffers(device, allocInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create command buffers: " << e.what() << std::endl; + return false; + } +} + +// Create sync objects +bool Renderer::createSyncObjects() { + try { + // Resize semaphores and fences vectors + imageAvailableSemaphores.clear(); + renderFinishedSemaphores.clear(); + inFlightFences.clear(); + + // 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; + } +} + +// 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(); + + // Clean up descriptor pool (this will automatically clean up descriptor sets) + descriptorPool = nullptr; + + // 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; +} + +// 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(); + createDepthResources(); + + // Recreate sync objects with correct sizing for new swap chain + createSyncObjects(); + + // Recreate descriptor pool and pipelines + createDescriptorPool(); + + // 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(); + + // Recreate descriptor sets for all entities after swapchain/pipeline rebuild + for (auto& [entity, resources] : entityResources) { + 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); + } +} + +// 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); +} + +// 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); +} + +// 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 + const std::vector& extractedLights = staticLights; + + if (!extractedLights.empty()) { + // Limit the number of active lights for performance + size_t numLights = std::min(extractedLights.size(), size_t(MAX_ACTIVE_LIGHTS)); + + // Create a subset of lights to upload this frame + std::vector lightsSubset; + lightsSubset.reserve(numLights); + for (size_t i = 0; i < numLights; ++i) { + lightsSubset.push_back(extractedLights[i]); + } + + // Apply UI-driven sun control to the first directional light with a Paris-based solar path + { + // Find first directional light to treat as the Sun + size_t sunIdx = SIZE_MAX; + for (size_t i = 0; i < lightsSubset.size(); ++i) { + if (lightsSubset[i].type == ExtractedLight::Type::Directional) { sunIdx = i; break; } + } + if (sunIdx != SIZE_MAX) { + auto &sun = lightsSubset[sunIdx]; + float s = std::clamp(sunPosition, 0.0f, 1.0f); + + // Paris latitude (degrees) + const float latDeg = 48.8566f; + const float lat = latDeg * 0.01745329251994329577f; // radians + + // Get current day-of-year (0..365) for declination + int yday = 172; // Default to around June solstice if time is unavailable + std::time_t now = std::time(nullptr); + if (now != (std::time_t)(-1)) { + std::tm localTm{}; + #ifdef _WIN32 + localtime_s(&localTm, &now); + #else + std::tm* ptm = std::localtime(&now); + if (ptm) localTm = *ptm; + #endif + if (localTm.tm_yday >= 0) yday = localTm.tm_yday; // 0-based + } + + // Solar declination (degrees) using Cooper's approximation + // δ = 23.45° * sin(360° * (284 + n) / 365) + float declDeg = 23.45f * std::sin((6.283185307179586f) * (284.0f + (float)(yday + 1)) / 365.0f); + float decl = declDeg * 0.01745329251994329577f; // radians + + // Map slider to local solar time (0..24h), hour angle H in radians (0 at noon) + float hours = s * 24.0f; + float Hdeg = (hours - 12.0f) * 15.0f; // degrees per hour + float H = Hdeg * 0.01745329251994329577f; // radians + + // Solar altitude (elevation) from spherical astronomy + // sin(alt) = sin φ sin δ + cos φ cos δ cos H + float sinAlt = std::sin(lat) * std::sin(decl) + std::cos(lat) * std::cos(decl) * std::cos(H); + sinAlt = std::clamp(sinAlt, -1.0f, 1.0f); + float alt = std::asin(sinAlt); // radians + + // Build horizontal azimuth basis from original sun direction (treat original as local solar noon azimuth) + glm::vec3 origDir = sun.direction; + glm::vec2 baseHoriz2 = glm::normalize(glm::vec2(origDir.x, origDir.z)); + if (!std::isfinite(baseHoriz2.x)) { baseHoriz2 = glm::vec2(0.0f, -1.0f); } + + // Rotate base horizontal around Y by hour angle H (east-west movement). Positive H -> afternoon (west) + float cosH = std::cos(H); + float sinH = std::sin(H); + glm::vec2 horizRot2 = glm::normalize(glm::vec2( + baseHoriz2.x * cosH - baseHoriz2.y * sinH, + baseHoriz2.x * sinH + baseHoriz2.y * cosH)); + + // Compose final direction from altitude and rotated horizontal + float cosAlt = std::cos(alt); + float sinAltClamped = std::sin(alt); + glm::vec3 newDir = glm::normalize(glm::vec3(horizRot2.x * cosAlt, -sinAltClamped, horizRot2.y * cosAlt)); + sun.direction = newDir; + + // Intensity scales with daylight (altitude); zero when below horizon + float dayFactor = std::max(0.0f, sinAltClamped); // 0..1 roughly + + // Warm tint increases near horizon when sun is above horizon + float horizonFactor = 0.0f; + if (sinAltClamped > 0.0f) { + // More warmth for low altitude, fade to zero near high noon + float normAlt = std::clamp(alt / (1.57079632679f), 0.0f, 1.0f); // 0 at horizon, 1 at zenith + horizonFactor = 1.0f - normAlt; // 1 near horizon, 0 at zenith + } + glm::vec3 warm(1.0f, 0.75f, 0.55f); + float tintAmount = 0.7f * horizonFactor; + sun.color = glm::mix(sun.color, warm, tintAmount); + + // Apply intensity scaling (preserve original magnitude shape) + sun.intensity *= dayFactor; + } + } + + // Update the light storage buffer with the subset of light data + updateLightStorageBuffer(currentImage, lightsSubset); + + ubo.lightCount = static_cast(numLights); + // Shadows removed: no shadow maps + } 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) + ubo.exposure = this->exposure; + ubo.gamma = this->gamma; + ubo.prefilteredCubeMipLevels = 0.0f; + ubo.scaleIBLAmbient = 0.5f; + + // Copy to uniform buffer + std::memcpy(entityIt->second.uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +} + +// Render the scene +void Renderer::Render(const std::vector>& entities, CameraComponent* camera, ImGuiSystem* imguiSystem) { + // Mark rendering as active (informational flag for systems that care) + if (memoryPool) { + memoryPool->setRenderingActive(true); + } + + // Use RAII to ensure rendering state is always reset, even if an exception occurs + struct RenderingStateGuard { + MemoryPool* pool; + explicit RenderingStateGuard(MemoryPool* p) : pool(p) {} + ~RenderingStateGuard() { + if (pool) { + pool->setRenderingActive(false); + } + } + } guard(memoryPool.get()); + + // Wait for the previous frame to finish + if (device.waitForFences(*inFlightFences[currentFrame], VK_TRUE, UINT64_MAX) != vk::Result::eSuccess) {} + + // Acquire the next image from the swap chain + uint32_t imageIndex; + // Use currentFrame for consistent semaphore indexing throughout acquire/submit/present chain + auto result = swapChain.acquireNextImage(UINT64_MAX, *imageAvailableSemaphores[currentFrame]); + imageIndex = result.second; + + // Check if the swap chain needs to be recreated + if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized) { + framebufferResized = false; + + // If ImGui has started a frame, we need to end it properly before returning + if (imguiSystem) { + ImGui::EndFrame(); + } + + recreateSwapChain(); + return; + } + if (result.first != vk::Result::eSuccess) { + throw std::runtime_error("Failed to acquire swap chain image"); + } + + // Reset the fence for the current frame + device.resetFences(*inFlightFences[currentFrame]); + + // Reset the command buffer + commandBuffers[currentFrame].reset(); + + // Record the command buffer + commandBuffers[currentFrame].begin(vk::CommandBufferBeginInfo()); + + // Update dynamic rendering attachments + colorAttachments[0].setImageView(*swapChainImageViews[imageIndex]); + depthAttachment.setImageView(*depthImageView); + + // Update rendering area + renderingInfo.setRenderArea(vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + + // Transition swapchain image layout for rendering + vk::ImageMemoryBarrier renderBarrier{ + .srcAccessMask = vk::AccessFlagBits::eNone, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eUndefined, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + commandBuffers[currentFrame].pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::DependencyFlags{}, + {}, + {}, + renderBarrier + ); + + // Begin dynamic rendering with vk::raii + commandBuffers[currentFrame].beginRendering(renderingInfo); + + // Set the viewport + vk::Viewport viewport(0.0f, 0.0f, + static_cast(swapChainExtent.width), + static_cast(swapChainExtent.height), + 0.0f, 1.0f); + commandBuffers[currentFrame].setViewport(0, viewport); + + // Set the scissor + vk::Rect2D scissor(vk::Offset2D(0, 0), swapChainExtent); + commandBuffers[currentFrame].setScissor(0, scissor); + + // Track current pipeline to avoid unnecessary bindings + vk::raii::Pipeline* currentPipeline = nullptr; + vk::raii::PipelineLayout* currentLayout = nullptr; + std::vector blendedQueue; + + // If loading, skip drawing scene entities (only clear and allow overlay) + bool blockScene = false; + if (imguiSystem) { + // Prefer renderer flag when available + blockScene = IsLoading() || (GetTextureTasksScheduled() > 0 && GetTextureTasksCompleted() < GetTextureTasksScheduled()); + } + + // Render each entity (skip while loading) + if (!blockScene) + for (auto const& uptr : entities) { + Entity* entity = uptr.get(); + if (!entity || !entity->IsActive()) { + continue; + } + // Check if ball-only rendering is enabled and filter entities accordingly + if (imguiSystem && imguiSystem->IsBallOnlyRenderingEnabled()) { + // Only render entities whose names contain "Ball_" + if (entity->GetName().find("Ball_") == std::string::npos) { + continue; // Skip non-ball entities + } + } + + // Skip camera entities - they should not be rendered + if (entity->GetName() == "Camera") { + continue; + } + + // Get the mesh component + auto meshComponent = entity->GetComponent(); + if (!meshComponent) { + continue; + } + + // Get the transform component + auto transformComponent = entity->GetComponent(); + if (!transformComponent) { + continue; + } + + // Determine which pipeline to use - now defaults to BRDF/PBR instead of Phong + // Use basic pipeline only when PBR is explicitly disabled via ImGui + bool useBasic = imguiSystem && !imguiSystem->IsPBREnabled(); + bool usePBR = !useBasic; // BRDF/PBR is now the default lighting model + + // Choose PBR pipeline variant per material (BLEND -> blended pipeline) + vk::raii::Pipeline* selectedPipeline = nullptr; + vk::raii::PipelineLayout* selectedLayout = nullptr; + if (usePBR) { + 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(); + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + if (material->alphaMode == "BLEND") { + useBlended = true; + } else if (material->alphaMode != "MASK" && material->transmissionFactor > 0.001f) { + // Use blended pipeline for transmissive materials + useBlended = true; + } else if (material->useSpecularGlossiness && material->alpha < 0.999f) { + // SpecGloss glass with alpha < 1 should blend + useBlended = true; + } + } + } + } + } + // Defer blended/transmissive materials to a second pass + if (useBlended) { + blendedQueue.push_back(entity); + continue; + } + // Opaques use the non-blended PBR pipeline in this first pass + selectedPipeline = &pbrGraphicsPipeline; + selectedLayout = &pbrPipelineLayout; + } else { + selectedPipeline = &graphicsPipeline; + selectedLayout = &pipelineLayout; + } + + // Get the mesh resources - they should already exist from pre-allocation + auto meshIt = meshResources.find(meshComponent); + if (meshIt == meshResources.end()) { + std::cerr << "ERROR: Mesh resources not found for entity " << entity->GetName() + << " - resources should have been pre-allocated during scene loading!" << std::endl; + continue; + } + + // Get the entity resources - they should already exist from pre-allocation + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) { + std::cerr << "ERROR: Entity resources not found for entity " << entity->GetName() + << " - resources should have been pre-allocated during scene loading!" << std::endl; + continue; + } + + // Bind pipeline if it changed + if (currentPipeline != selectedPipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); + currentPipeline = selectedPipeline; + currentLayout = selectedLayout; + } + + // Always bind both vertex and instance buffers since shaders expect instance data + // The instancing toggle controls the rendering behavior, not the buffer binding + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + + // Always set UBO.model from the entity's transform; shaders combine it with instance matrices + updateUniformBuffer(currentFrame, entity, camera); + + // Bind the index buffer + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + + // Bind the descriptor set using the appropriate pipeline layout + auto& selectedDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; + + // Check if descriptor sets exist for the current pipeline type + if (selectedDescriptorSets.empty()) { + std::cerr << "Error: No descriptor sets available for entity " << entity->GetName() + << " (pipeline: " << (usePBR ? "PBR" : "basic") << ")" << std::endl; + continue; // Skip this entity + } + + if (currentFrame >= selectedDescriptorSets.size()) { + std::cerr << "Error: Invalid frame index " << currentFrame + << " for entity " << entity->GetName() + << " (descriptor sets size: " << selectedDescriptorSets.size() << ")" << std::endl; + continue; // Skip this entity + } + + commandBuffers[currentFrame].bindDescriptorSets(vk::PipelineBindPoint::eGraphics, **currentLayout, 0, {*selectedDescriptorSets[currentFrame]}, {}); + + + // Set PBR material properties using push constants + if (usePBR) { + MaterialProperties pushConstants{}; + + // Try to get material properties for this specific entity + if (modelLoader && entity->GetName().find("_Material_") != std::string::npos) { + // Extract material name from entity name for any GLTF model entities + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + // After the tag, there should be a numeric material index, then an underscore, then the material name + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + // Use actual PBR properties from the GLTF material + pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); + pushConstants.metallicFactor = material->metallic; + pushConstants.roughnessFactor = material->roughness; + pushConstants.emissiveFactor = material->emissive; // Set emissive factor for HDR emissive sources + pushConstants.emissiveStrength = material->emissiveStrength; // Set emissive strength from KHR_materials_emissive_strength extension + pushConstants.transmissionFactor = material->transmissionFactor; // KHR_materials_transmission + // SpecGloss workflow push constants + if (material->useSpecularGlossiness) { + pushConstants.useSpecGlossWorkflow = 1; + pushConstants.specularFactor = material->specularFactor; + pushConstants.glossinessFactor = material->glossinessFactor; + // If no SpecGloss texture, signal shader to use factors-only path + pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + } else { + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.specularFactor = glm::vec3(0.04f); + pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; + pushConstants.physicalDescriptorTextureSet = 0; + } + } else { + // Default PBR material properties + pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); + pushConstants.metallicFactor = 0.1f; + pushConstants.roughnessFactor = 0.7f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 1.0f; + } + } + } + } else { + // Default PBR material properties for non-GLTF entities + pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); + pushConstants.metallicFactor = 0.1f; + pushConstants.roughnessFactor = 0.7f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 0.0f; + pushConstants.transmissionFactor = 0.0f; + // Default to MR workflow + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.specularFactor = glm::vec3(0.04f); + pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; + } + + // Set texture binding indices + pushConstants.baseColorTextureSet = 0; + pushConstants.physicalDescriptorTextureSet = 0; + pushConstants.normalTextureSet = 0; + pushConstants.occlusionTextureSet = 0; + // For emissive: indicate absence with -1 so shader uses factor-only emissive + int emissiveSet = -1; + if (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) { + emissiveSet = 0; + } + pushConstants.emissiveTextureSet = emissiveSet; + // Alpha mask from glTF material + pushConstants.alphaMask = 0.0f; + pushConstants.alphaMaskCutoff = 0.5f; + 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(); + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + if (material->alphaMode == "MASK") { + pushConstants.alphaMask = 1.0f; + pushConstants.alphaMaskCutoff = material->alphaCutoff; + } else { + pushConstants.alphaMask = 0.0f; // OPAQUE or BLEND not handled here + } + } + } + } + } + + // Push constants to the shader + commandBuffers[currentFrame].pushConstants( + **currentLayout, + vk::ShaderStageFlagBits::eFragment, + 0, + vk::ArrayProxy(sizeof(MaterialProperties), reinterpret_cast(&pushConstants)) + ); + } + + uint32_t instanceCount = static_cast(std::max(1u, static_cast(meshComponent->GetInstanceCount()))); + commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); + } + + // Second pass: render blended/transmissive materials after opaque + if (!blendedQueue.empty()) { + for (Entity* entity : blendedQueue) { + if (!entity || !entity->IsActive()) { continue; } + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) { continue; } + auto transformComponent = entity->GetComponent(); + if (!transformComponent) { continue; } + + // Mesh & entity resources + auto meshIt = meshResources.find(meshComponent); + if (meshIt == meshResources.end()) { + std::cerr << "ERROR: Mesh resources not found for blended entity " << entity->GetName() << std::endl; + continue; + } + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) { + std::cerr << "ERROR: Entity resources not found for blended entity " << entity->GetName() << std::endl; + continue; + } + + // Use blended PBR pipeline + vk::raii::Pipeline* selectedPipeline = &pbrBlendGraphicsPipeline; + vk::raii::PipelineLayout* selectedLayout = &pbrPipelineLayout; + if (currentPipeline != selectedPipeline) { + commandBuffers[currentFrame].bindPipeline(vk::PipelineBindPoint::eGraphics, **selectedPipeline); + currentPipeline = selectedPipeline; + currentLayout = selectedLayout; + } + + // Bind vertex + instance buffers + std::array buffers = {*meshIt->second.vertexBuffer, *entityIt->second.instanceBuffer}; + std::array offsets = {0, 0}; + commandBuffers[currentFrame].bindVertexBuffers(0, buffers, offsets); + + // Update UBO for this entity + updateUniformBuffer(currentFrame, entity, camera); + + // Bind index buffer + commandBuffers[currentFrame].bindIndexBuffer(*meshIt->second.indexBuffer, 0, vk::IndexType::eUint32); + + // Bind descriptor set (PBR) + auto& selectedDescriptorSets = entityIt->second.pbrDescriptorSets; + if (selectedDescriptorSets.empty() || currentFrame >= selectedDescriptorSets.size()) { + std::cerr << "Error: No valid PBR descriptor set for blended entity " << entity->GetName() << std::endl; + continue; + } + commandBuffers[currentFrame].bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, **currentLayout, 0, {*selectedDescriptorSets[currentFrame]}, {} + ); + + // Push PBR material properties (same as first pass) + MaterialProperties pushConstants{}; + 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(); + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material) { + pushConstants.baseColorFactor = glm::vec4(material->albedo, material->alpha); + pushConstants.metallicFactor = material->metallic; + pushConstants.roughnessFactor = material->roughness; + pushConstants.emissiveFactor = material->emissive; + pushConstants.emissiveStrength = material->emissiveStrength; + pushConstants.transmissionFactor = material->transmissionFactor; + if (material->useSpecularGlossiness) { + pushConstants.useSpecGlossWorkflow = 1; + pushConstants.specularFactor = material->specularFactor; + pushConstants.glossinessFactor = material->glossinessFactor; + pushConstants.physicalDescriptorTextureSet = material->specGlossTexturePath.empty() ? -1 : 0; + } else { + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.specularFactor = glm::vec3(0.04f); + pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; + pushConstants.physicalDescriptorTextureSet = 0; + } + } else { + pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); + pushConstants.metallicFactor = 0.1f; + pushConstants.roughnessFactor = 0.7f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 1.0f; + } + } + } + } else { + pushConstants.baseColorFactor = glm::vec4(0.8f, 0.8f, 0.8f, 1.0f); + pushConstants.metallicFactor = 0.1f; + pushConstants.roughnessFactor = 0.7f; + pushConstants.emissiveFactor = glm::vec3(0.0f); + pushConstants.emissiveStrength = 0.0f; + pushConstants.transmissionFactor = 0.0f; + pushConstants.useSpecGlossWorkflow = 0; + pushConstants.specularFactor = glm::vec3(0.04f); + pushConstants.glossinessFactor = 1.0f - pushConstants.roughnessFactor; + } + pushConstants.baseColorTextureSet = 0; + pushConstants.physicalDescriptorTextureSet = 0; + pushConstants.normalTextureSet = 0; + pushConstants.occlusionTextureSet = 0; + int emissiveSet = -1; + if (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) { emissiveSet = 0; } + pushConstants.emissiveTextureSet = emissiveSet; + pushConstants.alphaMask = 0.0f; + pushConstants.alphaMaskCutoff = 0.5f; + 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(); + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + Material* material = modelLoader->GetMaterial(materialName); + if (material && material->alphaMode == "MASK") { + pushConstants.alphaMask = 1.0f; + pushConstants.alphaMaskCutoff = material->alphaCutoff; + } + } + } + } + + commandBuffers[currentFrame].pushConstants( + **currentLayout, + vk::ShaderStageFlagBits::eFragment, + 0, + vk::ArrayProxy(sizeof(MaterialProperties), reinterpret_cast(&pushConstants)) + ); + + uint32_t instanceCount = static_cast(std::max(1u, static_cast(meshComponent->GetInstanceCount()))); + commandBuffers[currentFrame].drawIndexed(meshIt->second.indexCount, instanceCount, 0, 0, 0); + } + } + + // Render ImGui if provided + if (imguiSystem) { + imguiSystem->Render(commandBuffers[currentFrame], currentFrame); + } + + // End dynamic rendering + commandBuffers[currentFrame].endRendering(); + + // Transition swapchain image layout for presentation + vk::ImageMemoryBarrier imageBarrier{ + .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + .dstAccessMask = vk::AccessFlagBits::eNone, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::ePresentSrcKHR, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = swapChainImages[imageIndex], + .subresourceRange = { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1 + } + }; + + commandBuffers[currentFrame].pipelineBarrier( + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eBottomOfPipe, + vk::DependencyFlags{}, + {}, + {}, + imageBarrier + ); + + // End command buffer + commandBuffers[currentFrame].end(); + + // Submit command buffer + // Wait for both image availability (binary) and all completed texture uploads (timeline) + std::array waitSems = { *imageAvailableSemaphores[currentFrame], *uploadsTimeline }; + std::array waitStages = { vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader }; + uint64_t uploadsValueToWait = uploadTimelineValue.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] + }; + + // Use mutex to ensure thread-safe access to graphics queue + { + std::lock_guard lock(queueMutex); + graphicsQueue.submit(submitInfo, *inFlightFences[currentFrame]); + } + + // Present the image + vk::PresentInfoKHR presentInfo{ + .waitSemaphoreCount = 1, + .pWaitSemaphores = &*renderFinishedSemaphores[imageIndex], + .swapchainCount = 1, + .pSwapchains = &*swapChain, + .pImageIndices = &imageIndex + }; + + // Use mutex to ensure thread-safe access to present queue + try { + std::lock_guard lock(queueMutex); + result.first = presentQueue.presentKHR(presentInfo); + } catch (const vk::OutOfDateKHRError&) { + framebufferResized = true; + } + + if (result.first == vk::Result::eErrorOutOfDateKHR || result.first == vk::Result::eSuboptimalKHR || framebufferResized) { + framebufferResized = false; + recreateSwapChain(); + } else if (result.first != vk::Result::eSuccess) { + throw std::runtime_error("Failed to present swap chain image"); + } + + // Advance to the next frame + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} diff --git a/attachments/simple_engine/renderer_resources.cpp b/attachments/simple_engine/renderer_resources.cpp new file mode 100644 index 00000000..caba414b --- /dev/null +++ b/attachments/simple_engine/renderer_resources.cpp @@ -0,0 +1,2148 @@ +#include "renderer.h" +#include "model_loader.h" +#include "mesh_component.h" +#include "transform_component.h" +#include +#include +#include +#include +#include +#include +#include + +// stb_image dependency removed; all GLTF textures are uploaded via memory path from ModelLoader. + +// KTX2 support +#include + +// This file contains resource-related methods from the Renderer class + +// Define shared default PBR texture identifiers (static constants) +const std::string Renderer::SHARED_DEFAULT_ALBEDO_ID = "__shared_default_albedo__"; +const std::string Renderer::SHARED_DEFAULT_NORMAL_ID = "__shared_default_normal__"; +const std::string Renderer::SHARED_DEFAULT_METALLIC_ROUGHNESS_ID = "__shared_default_metallic_roughness__"; +const std::string Renderer::SHARED_DEFAULT_OCCLUSION_ID = "__shared_default_occlusion__"; +const std::string Renderer::SHARED_DEFAULT_EMISSIVE_ID = "__shared_default_emissive__"; +const std::string Renderer::SHARED_BRIGHT_RED_ID = "__shared_bright_red__"; + +// Create depth resources +bool Renderer::createDepthResources() { + try { + // Find depth format + vk::Format depthFormat = findDepthFormat(); + + // 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; + } +} + +// 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.find(textureId) != texturesLoading.end()) { + 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>((void*)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)) { + 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 { + std::filesystem::path cand = std::filesystem::path(dir) / candidateName; + if (std::filesystem::exists(cand)) { + std::string candStr = cand.string(); + std::cout << "Retrying KTX2 load with sibling candidate '" << candStr << "' for original '" << resolvedPath << "'" << std::endl; + result = ktxTexture2_CreateFromNamedFile(candStr.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex); + if (result == KTX_SUCCESS) { + resolvedPath = candStr; // Use the successfully opened candidate + return true; + } + } + return false; + }; + // Known suffix variants near the end of filename before extension + std::vector suffixes = {"_c", "_d", "_cm", "_diffuse", "_basecolor", "_albedo"}; + for (const auto& s : suffixes) { + std::string key = s + ".ktx2"; + if (fname.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((ktxTexture*)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(); + + // Free pixel data + if (isKtx2) { + ktxTexture_Destroy((ktxTexture*)ktxTex); + } else { + // no-op: non-KTX path disabled + } + + // Determine appropriate texture format + vk::Format textureFormat; + if (isKtx2) { + // If the KTX2 provided a valid VkFormat and we did NOT transcode, use it (may be a GPU compressed format) + if (!wasTranscoded) { + VkFormat headerFmt = static_cast(headerVkFormatRaw); + if (headerFmt != VK_FORMAT_UNDEFINED) { + textureFormat = static_cast(headerFmt); + } else { + textureFormat = Renderer::determineTextureFormat(textureId); + } + } else { + // Transcoded to RGBA32; choose SRGB/UNORM by heuristic + textureFormat = Renderer::determineTextureFormat(textureId); + } + } else { + textureFormat = Renderer::determineTextureFormat(textureId); + } + + + // 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 (no global serialization) + uploadImageFromStaging(*stagingBuffer, *resources.textureImage, textureFormat, copyRegions, mipLevels); + + // Store the format and mipLevels for createTextureImageView + resources.format = textureFormat; + resources.mipLevels = mipLevels; + + // 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; + } +} + +// 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; + } +} + +// 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; + } +} + +// 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; + } +} + +// 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; +} + +// 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::transform(idLower.begin(), idLower.end(), 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; + } + + // All other PBR textures (normal, metallic-roughness, occlusion, emissive) 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.find(resolvedId) != texturesLoading.end()) { + 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>((void*)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; + } + + 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 (no global serialization). 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; + + // 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; + } +} + +// Create mesh resources +bool Renderer::createMeshResources(MeshComponent* meshComponent) { + ensureThreadLocalVulkanInit(); + try { + // Check if mesh resources already exist + 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; + } + + // Create vertex buffer + vk::DeviceSize vertexBufferSize = sizeof(vertices[0]) * vertices.size(); + auto [stagingVertexBuffer, stagingVertexBufferMemory] = createBuffer( + vertexBufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy vertex data to staging buffer + void* vertexData = stagingVertexBufferMemory.mapMemory(0, vertexBufferSize); + memcpy(vertexData, vertices.data(), static_cast(vertexBufferSize)); + stagingVertexBufferMemory.unmapMemory(); + + // Create vertex buffer on device using memory pool + auto [vertexBuffer, vertexBufferAllocation] = createBufferPooled( + vertexBufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + // Copy from staging buffer to device buffer + copyBuffer(stagingVertexBuffer, vertexBuffer, vertexBufferSize); + + // Create index buffer + vk::DeviceSize indexBufferSize = sizeof(indices[0]) * indices.size(); + auto [stagingIndexBuffer, stagingIndexBufferMemory] = createBuffer( + indexBufferSize, + vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ); + + // Copy index data to staging buffer + void* indexData = stagingIndexBufferMemory.mapMemory(0, indexBufferSize); + memcpy(indexData, indices.data(), static_cast(indexBufferSize)); + stagingIndexBufferMemory.unmapMemory(); + + // Create index buffer on device using memory pool + auto [indexBuffer, indexBufferAllocation] = createBufferPooled( + indexBufferSize, + vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal + ); + + // Copy from staging buffer to device buffer + copyBuffer(stagingIndexBuffer, indexBuffer, indexBufferSize); + + // Create mesh resources + 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()); + + // 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; + } +} + +// 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 = { + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eUniformBuffer, + .descriptorCount = uboDescriptors + }, + vk::DescriptorPoolSize{ + .type = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = textureDescriptors + }, + 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; + } +} + +// 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); + // Guard textureResources access while resolving texture handles + std::shared_lock texLock(textureResourcesMutex); + try { + // Get entity resources + auto entityIt = entityResources.find(entity); + if (entityIt == entityResources.end()) { + std::cerr << "Entity resources not found" << std::endl; + return false; + } + + // Get texture resources - use default texture as fallback if specific texture fails + TextureResources* textureRes = nullptr; + if (!texturePath.empty()) { + auto textureIt = textureResources.find(resolvedTexturePath); + if (textureIt == textureResources.end()) { + // If this is a GLTF embedded texture ID, don't try to load from disk + if (texturePath.rfind("gltf_", 0) == 0) { + // Handle both gltf_baseColor_{i} and gltf_basecolor_{i} + const std::string prefixUpper = "gltf_baseColor_"; + const std::string prefixLower = "gltf_basecolor_"; + if (texturePath.rfind(prefixUpper, 0) == 0 || texturePath.rfind(prefixLower, 0) == 0) { + const bool isUpper = texturePath.rfind(prefixUpper, 0) == 0; + std::string index = texturePath.substr((isUpper ? prefixUpper.size() : prefixLower.size())); + // Try direct baseColor id first + std::string baseColorId = "gltf_baseColor_" + index; + auto bcIt = textureResources.find(baseColorId); + if (bcIt != textureResources.end()) { + textureRes = &bcIt->second; + } else { + // Try alias to generic gltf_texture_{index} + std::string alias = "gltf_texture_" + index; + auto aliasIt = textureResources.find(alias); + if (aliasIt != textureResources.end()) { + textureRes = &aliasIt->second; + } else { + std::cerr << "Warning: Embedded texture not found: " << texturePath + << " (also missing alias: " << alias << ") using default." << std::endl; + textureRes = &defaultTextureResources; + } + } + } else { + std::cerr << "Warning: Embedded texture not found: " << texturePath << ", using default." << std::endl; + textureRes = &defaultTextureResources; + } + } else { + // Texture not yet available; bind default texture for now. + textureRes = &defaultTextureResources; + } + } else { + textureRes = &textureIt->second; + } + } else { + // No texture path specified, use default texture + textureRes = &defaultTextureResources; + } + + // Create descriptor sets using RAII - choose layout based on pipeline type + vk::DescriptorSetLayout selectedLayout = usePBR ? *pbrDescriptorSetLayout : *descriptorSetLayout; + std::vector layouts(MAX_FRAMES_IN_FLIGHT, selectedLayout); + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT), + .pSetLayouts = layouts.data() + }; + + // Choose the appropriate descriptor set vector based on pipeline type + auto& targetDescriptorSets = usePBR ? entityIt->second.pbrDescriptorSets : entityIt->second.basicDescriptorSets; + + // Only create descriptor sets if they don't already exist for this pipeline type + if (targetDescriptorSets.empty()) { + try { + // Allocate descriptor sets using RAII wrapper + vk::raii::DescriptorSets raiiDescriptorSets(device, allocInfo); + + // Convert to vector of individual RAII descriptor sets + targetDescriptorSets.clear(); + targetDescriptorSets.reserve(MAX_FRAMES_IN_FLIGHT); + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + targetDescriptorSets.emplace_back(std::move(raiiDescriptorSets[i])); + } + } catch (const std::exception& e) { + std::cerr << "Failed to allocate descriptor sets for entity " << entity->GetName() + << " (pipeline: " << (usePBR ? "PBR" : "basic") << "): " << e.what() << std::endl; + return false; + } + } + + // Validate descriptor sets before using them + if (targetDescriptorSets.size() != MAX_FRAMES_IN_FLIGHT) { + std::cerr << "Invalid descriptor set count for entity " << entity->GetName() + << " (expected: " << MAX_FRAMES_IN_FLIGHT << ", got: " << targetDescriptorSets.size() << ")" << std::endl; + return false; + } + + // Validate default texture resources before using them + if (!*defaultTextureResources.textureSampler || !*defaultTextureResources.textureImageView) { + std::cerr << "Invalid default texture resources for entity " << entity->GetName() << std::endl; + return false; + } + + // Update descriptor sets + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + // Validate descriptor set handle before using it + if (!*targetDescriptorSets[i]) { + std::cerr << "Invalid descriptor set handle for entity " << entity->GetName() + << " at frame " << i << " (pipeline: " << (usePBR ? "PBR" : "basic") << ")" << std::endl; + return false; + } + + // Validate uniform buffer before creating descriptor + if (i >= entityIt->second.uniformBuffers.size() || + !*entityIt->second.uniformBuffers[i]) { + std::cerr << "Invalid uniform buffer for entity " << entity->GetName() + << " at frame " << i << std::endl; + return false; + } + + // Uniform buffer descriptor + vk::DescriptorBufferInfo bufferInfo{ + .buffer = *entityIt->second.uniformBuffers[i], + .offset = 0, + .range = sizeof(UniformBufferObject) + }; + + if (usePBR) { + // PBR pipeline: Create 7 descriptor writes (UBO + 5 textures + light storage buffer) + std::array descriptorWrites; + std::array imageInfos; + + // Uniform buffer descriptor writes (binding 0) + descriptorWrites[0] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo + }; + + // Get all PBR texture paths from the entity's MeshComponent + auto meshComponent = entity->GetComponent(); + // Resolve baseColor path with multiple fallbacks: GLTF baseColor -> legacy texturePath -> material DB -> shared default + std::string resolvedBaseColor; + if (meshComponent && !meshComponent->GetBaseColorTexturePath().empty()) { + resolvedBaseColor = meshComponent->GetBaseColorTexturePath(); + } else if (meshComponent && !meshComponent->GetTexturePath().empty()) { + resolvedBaseColor = meshComponent->GetTexturePath(); + } else { + // Try to use material name from entity name to query ModelLoader + std::string entityName = entity->GetName(); + size_t tagPos = entityName.find("_Material_"); + if (tagPos != std::string::npos) { + size_t afterTag = tagPos + std::string("_Material_").size(); + // Expect format: _Material__ + size_t sep = entityName.find('_', afterTag); + if (sep != std::string::npos && sep + 1 < entityName.length()) { + std::string materialName = entityName.substr(sep + 1); + if (modelLoader) { + Material* mat = modelLoader->GetMaterial(materialName); + if (mat && !mat->albedoTexturePath.empty()) { + resolvedBaseColor = mat->albedoTexturePath; + } + } + } + } + if (resolvedBaseColor.empty()) { + resolvedBaseColor = SHARED_DEFAULT_ALBEDO_ID; + } + } + + // Heuristic: if still default and we have an external normal map like *_ddna.ktx2, try to guess base color sibling + if (resolvedBaseColor == SHARED_DEFAULT_ALBEDO_ID && meshComponent) { + std::string normalPath = meshComponent->GetNormalTexturePath(); + if (!normalPath.empty() && normalPath.rfind("gltf_", 0) != 0) { + // Make a lowercase copy for pattern checks + std::string normalLower = normalPath; + std::transform(normalLower.begin(), normalLower.end(), normalLower.begin(), [](unsigned char c){ return static_cast(std::tolower(c)); }); + if (normalLower.find("_ddna") != std::string::npos) { + // Try replacing _ddna with common diffuse/basecolor suffixes + std::vector suffixes = {"_d", "_c", "_cm", "_diffuse", "_basecolor"}; + for (const auto& suf : suffixes) { + std::string candidate = normalPath; + // Replace only the first occurrence of _ddna + size_t pos = normalLower.find("_ddna"); + if (pos != std::string::npos) { + candidate.replace(pos, 5, suf); + // Attempt to load; if successful, use this as resolved base color + // On-demand loading disabled; skip attempting to load candidate + (void)candidate; // suppress unused + break; + } + } + } + } + } + + std::vector pbrTexturePaths = { + // Binding 1: baseColor + resolvedBaseColor, + // Binding 2: metallicRoughness - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetMetallicRoughnessTexturePath().empty()) ? + meshComponent->GetMetallicRoughnessTexturePath() : SHARED_DEFAULT_METALLIC_ROUGHNESS_ID, + // Binding 3: normal - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetNormalTexturePath().empty()) ? + meshComponent->GetNormalTexturePath() : SHARED_DEFAULT_NORMAL_ID, + // Binding 4: occlusion - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetOcclusionTexturePath().empty()) ? + meshComponent->GetOcclusionTexturePath() : SHARED_DEFAULT_OCCLUSION_ID, + // Binding 5: emissive - use GLTF texture or fallback to shared default + (meshComponent && !meshComponent->GetEmissiveTexturePath().empty()) ? + meshComponent->GetEmissiveTexturePath() : SHARED_DEFAULT_EMISSIVE_ID + }; + + // Create image infos for each PBR texture binding + for (int j = 0; j < 5; j++) { + const std::string& currentTexturePath = pbrTexturePaths[j]; + const std::string resolvedBindingPath = ResolveTextureId(currentTexturePath); + + // Find the texture resources for this binding + auto textureIt = textureResources.find(resolvedBindingPath); + if (textureIt != textureResources.end()) { + // Use the specific texture for this binding + const auto& texRes = textureIt->second; + + // Validate texture resources before using them (check if RAII objects are valid) + if (*texRes.textureSampler == VK_NULL_HANDLE || *texRes.textureImageView == VK_NULL_HANDLE) { + std::cerr << "Invalid texture resources for " << currentTexturePath + << " in entity " << entity->GetName() << ", using default texture" << std::endl; + // Fall back to default texture + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } else { + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *texRes.textureSampler, + .imageView = *texRes.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } + } else { + // On-demand texture loading disabled; use alias or defaults below + // Try alias for embedded baseColor textures: gltf_baseColor_{i} -> gltf_texture_{i} + if (currentTexturePath.rfind("gltf_baseColor_", 0) == 0 || + currentTexturePath.rfind("gltf_basecolor_", 0) == 0) { + std::string prefix = (currentTexturePath.rfind("gltf_baseColor_", 0) == 0) + ? std::string("gltf_baseColor_") + : std::string("gltf_basecolor_"); + std::string index = currentTexturePath.substr(prefix.size()); + std::string alias = "gltf_texture_" + index; + auto aliasIt = textureResources.find(alias); + if (aliasIt != textureResources.end()) { + const auto& texRes = aliasIt->second; + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *texRes.textureSampler, + .imageView = *texRes.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } else { + // Fall back to default white texture if the specific texture is not found + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } + } else { + // Fall back to default white texture if the specific texture is not found + imageInfos[j] = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } + } + descriptor_path_resolved: ; + } + + // Create descriptor writes for all 5 texture bindings + for (int binding = 1; binding <= 5; binding++) { + descriptorWrites[binding] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = static_cast(binding), + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfos[binding - 1] + }; + } + + // No shadow maps: binding 6 is now the light storage buffer + + // Create descriptor write for light storage buffer (binding 6) + // Check if light storage buffers are initialized + if (i < lightStorageBuffers.size() && *lightStorageBuffers[i].buffer) { + vk::DescriptorBufferInfo lightBufferInfo{ + .buffer = *lightStorageBuffers[i].buffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + descriptorWrites[6] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 6, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &lightBufferInfo + }; + } else { + // Ensure light storage buffers are initialized before creating descriptor sets + // Initialize with at least 1 light to create the buffers + if (!createOrResizeLightStorageBuffers(1)) { + std::cerr << "Failed to initialize light storage buffers for descriptor set creation" << std::endl; + return false; + } + + // Now use the properly initialized light storage buffer + vk::DescriptorBufferInfo lightBufferInfo{ + .buffer = *lightStorageBuffers[i].buffer, + .offset = 0, + .range = VK_WHOLE_SIZE + }; + + descriptorWrites[6] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 6, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eStorageBuffer, + .pBufferInfo = &lightBufferInfo + }; + } + + // Update descriptor sets with all 7 descriptors + device.updateDescriptorSets(descriptorWrites, {}); + } else { + // Basic pipeline: Create 2 descriptor writes (UBO + 1 texture) + std::array descriptorWrites; + + // Uniform buffer descriptor write + descriptorWrites[0] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo + }; + + // Check if texture resources are valid + bool hasValidTexture = !texturePath.empty() && textureRes && + *textureRes->textureSampler && + *textureRes->textureImageView; + + // Texture sampler descriptor + vk::DescriptorImageInfo imageInfo; + if (hasValidTexture) { + // Use provided texture resources + imageInfo = vk::DescriptorImageInfo{ + .sampler = *textureRes->textureSampler, + .imageView = *textureRes->textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } else { + // Use default texture resources + imageInfo = vk::DescriptorImageInfo{ + .sampler = *defaultTextureResources.textureSampler, + .imageView = *defaultTextureResources.textureImageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + } + + // Texture sampler descriptor write + descriptorWrites[1] = vk::WriteDescriptorSet{ + .dstSet = targetDescriptorSets[i], + .dstBinding = 1, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; + + // Update descriptor sets with both descriptors + device.updateDescriptorSets(descriptorWrites, {}); + } + } + + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to create descriptor sets: " << 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; + } +} + +// Create buffer using memory pool for efficient allocation +std::pair> Renderer::createBufferPooled( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties) { + try { + if (!memoryPool) { + throw std::runtime_error("Memory pool not initialized"); + } + + // Use memory pool for allocation + auto [buffer, allocation] = memoryPool->createBuffer(size, usage, properties); + + return {std::move(buffer), std::move(allocation)}; + + } catch (const std::exception& e) { + std::cerr << "Failed to create buffer with memory pool: " << e.what() << std::endl; + throw; + } +} + +// Legacy createBuffer function - now strictly enforces memory pool usage +std::pair Renderer::createBuffer( + vk::DeviceSize size, + vk::BufferUsageFlags usage, + vk::MemoryPropertyFlags properties) { + + // This function should only be used for temporary staging buffers during resource creation + // All persistent resources should use createBufferPooled directly + + if (!memoryPool) { + throw std::runtime_error("Memory pool not available - cannot create buffer"); + } + + + // Only allow direct allocation for staging buffers (temporary, host-visible) + if (!(properties & vk::MemoryPropertyFlagBits::eHostVisible)) { + std::cerr << "ERROR: Legacy createBuffer should only be used for staging buffers!" << std::endl; + throw std::runtime_error("Legacy createBuffer used for non-staging buffer"); + } + + try { + vk::BufferCreateInfo bufferInfo{ + .size = size, + .usage = usage, + .sharingMode = vk::SharingMode::eExclusive + }; + + vk::raii::Buffer buffer(device, bufferInfo); + + // Allocate memory directly for staging buffers only + vk::MemoryRequirements memRequirements = buffer.getMemoryRequirements(); + + // Align allocation size to nonCoherentAtomSize (64 bytes) to prevent validation errors + // VUID-VkMappedMemoryRange-size-01390 requires memory flush sizes to be multiples of nonCoherentAtomSize + const vk::DeviceSize nonCoherentAtomSize = 64; // Typical value, should query from device properties + vk::DeviceSize alignedSize = ((memRequirements.size + nonCoherentAtomSize - 1) / nonCoherentAtomSize) * nonCoherentAtomSize; + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = alignedSize, + .memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties) + }; + + vk::raii::DeviceMemory bufferMemory(device, allocInfo); + + // Bind memory to buffer + buffer.bindMemory(*bufferMemory, 0); + + return {std::move(buffer), std::move(bufferMemory)}; + + } catch (const std::exception& e) { + std::cerr << "Failed to create staging buffer: " << e.what() << std::endl; + throw; + } +} + +// 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); + } + 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; + } +} + +// 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; + } +} + +// 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; + } +} + +// 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=" << (int)oldLayout << " new=" << (int)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); + } + 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 transfer queue + 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 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); + transferQueue.submit(submitInfo, *fence); + } + 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, size_t(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; + } +} + +// 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& [entity, resources] : entityResources) { + // 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; + } +} + +// 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) { + if (texturePath.empty()) { + return std::async(std::launch::deferred, [] { return false; }); + } + // Schedule the load without dropping tasks; throttling is handled during GPU upload + textureTasksScheduled.fetch_add(1, std::memory_order_relaxed); + auto task = [this, texturePath]() { + bool ok = this->LoadTexture(texturePath); + textureTasksCompleted.fetch_add(1, std::memory_order_relaxed); + return ok; + }; + 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) { + 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); + auto task = [this, textureId, data = std::move(dataCopy), width, height, channels]() mutable { + bool ok = this->LoadTextureFromMemory(textureId, data.data(), width, height, channels); + textureTasksCompleted.fetch_add(1, std::memory_order_relaxed); + return ok; + }; + if (!threadPool) { + return std::async(std::launch::async, std::move(task)); + } + return threadPool->enqueue(std::move(task)); +} + + +// 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 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 + 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; + toShader.dstAccessMask = vk::AccessFlagBits::eNone; // cannot use ShaderRead on transfer queue; visibility handled via timeline wait on graphics + cb.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eTransfer, + vk::DependencyFlagBits::eByRegion, + nullptr, nullptr, toShader); + + cb.end(); + + // Submit once on the transfer queue, signal timeline semaphore, and wait fence (for safety) + uint64_t signalValue = uploadTimelineValue.fetch_add(1, std::memory_order_relaxed) + 1; + vk::TimelineSemaphoreSubmitInfo timelineInfo{ + .signalSemaphoreValueCount = 1, + .pSignalSemaphoreValues = &signalValue + }; + vk::SubmitInfo submit{ + .pNext = &timelineInfo, + .commandBufferCount = 1, + .pCommandBuffers = &*cb, + .signalSemaphoreCount = 1, + .pSignalSemaphores = &*uploadsTimeline + }; + vk::raii::Fence fence(device, vk::FenceCreateInfo{}); + // Prefer dedicated transfer queue + { + std::lock_guard lock(queueMutex); + transferQueue.submit(submit, *fence); + } + device.waitForFences({*fence}, VK_TRUE, UINT64_MAX); + } catch (const std::exception& e) { + std::cerr << "uploadImageFromStaging failed: " << e.what() << std::endl; + throw; + } +} diff --git a/attachments/simple_engine/renderer_utils.cpp b/attachments/simple_engine/renderer_utils.cpp new file mode 100644 index 00000000..5b98b0a4 --- /dev/null +++ b/attachments/simple_engine/renderer_utils.cpp @@ -0,0 +1,288 @@ +#include "renderer.h" +#include +#include +#include +#include +#include + +// This file contains utility methods from the Renderer class + +// Find memory type +uint32_t Renderer::findMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) const { + try { + // Get memory properties + vk::PhysicalDeviceMemoryProperties memProperties = physicalDevice.getMemoryProperties(); + + // Find suitable memory type + for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } + } + + throw std::runtime_error("Failed to find suitable memory type"); + } catch (const std::exception& e) { + std::cerr << "Failed to find memory type: " << e.what() << std::endl; + throw; + } +} + +// Find supported format +vk::Format Renderer::findSupportedFormat(const std::vector& candidates, vk::ImageTiling tiling, vk::FormatFeatureFlags features) { + try { + for (vk::Format format : candidates) { + vk::FormatProperties props = physicalDevice.getFormatProperties(format); + + if (tiling == vk::ImageTiling::eLinear && (props.linearTilingFeatures & features) == features) { + return format; + } else if (tiling == vk::ImageTiling::eOptimal && (props.optimalTilingFeatures & features) == features) { + return format; + } + } + + throw std::runtime_error("Failed to find supported format"); + } catch (const std::exception& e) { + std::cerr << "Failed to find supported format: " << e.what() << std::endl; + throw; + } +} + +// Find depth format +vk::Format Renderer::findDepthFormat() { + try { + vk::Format depthFormat = findSupportedFormat( + {vk::Format::eD32Sfloat, vk::Format::eD32SfloatS8Uint, vk::Format::eD24UnormS8Uint}, + vk::ImageTiling::eOptimal, + vk::FormatFeatureFlagBits::eDepthStencilAttachment + ); + std::cout << "Found depth format: " << static_cast(depthFormat) << std::endl; + return depthFormat; + } catch (const std::exception& e) { + std::cerr << "Failed to find supported depth format, falling back to D32_SFLOAT: " << e.what() << std::endl; + // Fallback to D32_SFLOAT which is widely supported + return vk::Format::eD32Sfloat; + } +} + +// Check if format has stencil component +bool Renderer::hasStencilComponent(vk::Format format) { + return format == vk::Format::eD32SfloatS8Uint || format == vk::Format::eD24UnormS8Uint; +} + +// Read file +std::vector Renderer::readFile(const std::string& filename) { + try { + // Open file at end to get size + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + filename); + } + + // Get file size + size_t fileSize = file.tellg(); + std::vector buffer(fileSize); + + // Go back to beginning of file and read data + file.seekg(0); + file.read(buffer.data(), fileSize); + + // Close file + file.close(); + + return buffer; + } catch (const std::exception& e) { + std::cerr << "Failed to read file: " << e.what() << std::endl; + throw; + } +} + +// Create shader module +vk::raii::ShaderModule Renderer::createShaderModule(const std::vector& code) { + try { + // Create shader module + vk::ShaderModuleCreateInfo createInfo{ + .codeSize = code.size(), + .pCode = reinterpret_cast(code.data()) + }; + + return vk::raii::ShaderModule(device, createInfo); + } catch (const std::exception& e) { + std::cerr << "Failed to create shader module: " << e.what() << std::endl; + throw; + } +} + +// Find queue families +QueueFamilyIndices Renderer::findQueueFamilies(const vk::raii::PhysicalDevice& device) { + QueueFamilyIndices indices; + + // Get queue family properties + std::vector queueFamilies = device.getQueueFamilyProperties(); + + // Find queue families that support graphics, compute, present, and (optionally) a dedicated transfer queue + for (uint32_t i = 0; i < queueFamilies.size(); i++) { + const auto& qf = queueFamilies[i]; + // Check for graphics support + if ((qf.queueFlags & vk::QueueFlagBits::eGraphics) && !indices.graphicsFamily.has_value()) { + indices.graphicsFamily = i; + } + // Check for compute support + if ((qf.queueFlags & vk::QueueFlagBits::eCompute) && !indices.computeFamily.has_value()) { + indices.computeFamily = i; + } + // 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; + + // Get surface capabilities + details.capabilities = device.getSurfaceCapabilitiesKHR(surface); + + // Get surface formats + details.formats = device.getSurfaceFormatsKHR(surface); + + // Get present modes + details.presentModes = device.getSurfacePresentModesKHR(surface); + + return details; +} + +// Check device extension support +bool Renderer::checkDeviceExtensionSupport(vk::raii::PhysicalDevice& device) { + auto availableDeviceExtensions = device.enumerateDeviceExtensionProperties(); + + // 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.find(extension.extensionName) != optionalExtensionsSet.end()) { + std::cout << " " << extension.extensionName << " (supported)" << std::endl; + } + } + + return true; +} + +// Check if device is suitable +bool Renderer::isDeviceSuitable(vk::raii::PhysicalDevice& device) { + // Check queue families + QueueFamilyIndices indices = findQueueFamilies(device); + + // Check device extensions + bool extensionsSupported = checkDeviceExtensionSupport(device); + + // Check swap chain support + bool swapChainAdequate = false; + if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); + } + + // Check for required features + auto features = device.template getFeatures2(); + bool supportsRequiredFeatures = features.template get().dynamicRendering; + + return indices.isComplete() && extensionsSupported && swapChainAdequate && supportsRequiredFeatures; +} + +// Choose swap surface format +vk::SurfaceFormatKHR Renderer::chooseSwapSurfaceFormat(const std::vector& availableFormats) { + // Look for SRGB format + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == vk::Format::eB8G8R8A8Srgb && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) { + return availableFormat; + } + } + + // If not found, return first available format + return availableFormats[0]; +} + +// Choose swap present mode +vk::PresentModeKHR Renderer::chooseSwapPresentMode(const std::vector& availablePresentModes) { + // Look for mailbox mode (triple buffering) + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == vk::PresentModeKHR::eMailbox) { + return availablePresentMode; + } + } + + // If not found, return FIFO mode (guaranteed to be available) + return vk::PresentModeKHR::eFifo; +} + +// Choose swap extent +vk::Extent2D Renderer::chooseSwapExtent(const vk::SurfaceCapabilitiesKHR& capabilities) { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } else { + // Get framebuffer size + int width, height; + platform->GetWindowSize(&width, &height); + + // Create extent + vk::Extent2D actualExtent = { + static_cast(width), + static_cast(height) + }; + + // Clamp to min/max extent + actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + + return actualExtent; + } +} + +// Wait for device to be idle +void Renderer::WaitIdle() { + device.waitIdle(); +} + diff --git a/attachments/simple_engine/resource_manager.cpp b/attachments/simple_engine/resource_manager.cpp new file mode 100644 index 00000000..74159a2b --- /dev/null +++ b/attachments/simple_engine/resource_manager.cpp @@ -0,0 +1,26 @@ +#include "resource_manager.h" + +// Most of the ResourceManager class implementation is in the header file +// This file is mainly for any methods that might need additional implementation +// +// 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; +} + +void Resource::Unload() { + loaded = false; +} + +void ResourceManager::UnloadAllResources() { + for (auto& typePair : resources) { + for (auto& resourcePair : typePair.second) { + resourcePair.second->Unload(); + } + typePair.second.clear(); + } + resources.clear(); +} diff --git a/attachments/simple_engine/resource_manager.h b/attachments/simple_engine/resource_manager.h new file mode 100644 index 00000000..3dadbee7 --- /dev/null +++ b/attachments/simple_engine/resource_manager.h @@ -0,0 +1,252 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * @brief Base class for all resources. + */ +class Resource { +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(); } +}; + +/** + * @brief Class for managing resources. + * + * 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 { +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.find(id) != typeResources.end(); + } + + /** + * @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 +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 new file mode 100644 index 00000000..79774080 --- /dev/null +++ b/attachments/simple_engine/scene_loading.cpp @@ -0,0 +1,270 @@ +#include "scene_loading.h" +#include "engine.h" +#include "transform_component.h" +#include "mesh_component.h" +#include "camera_component.h" +#include +#include +#include +#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; +} + +/** + * @brief Load a GLTF model synchronously on the main thread. + * @param engine The engine to create entities in. + * @param modelPath The path to the GLTF model file. + * @param position The position to place the model (default: origin with slight Y offset). + * @param rotation The rotation to apply to the model (default: no rotation). + * @param scale The scale to apply to the model (default: unit scale). + */ +void 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; + } + // Ensure loading flag is cleared on any exit from this function + struct LoadingGuard { Renderer* r; ~LoadingGuard(){ if (r) 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; + } + + 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 + for (auto& light : extractedLights) { + glm::vec4 worldPos = transformMatrix * glm::vec4(light.position, 1.0f); + light.position = glm::vec3(worldPos); + + // Also transform the light direction (for directional lights) + glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(transformMatrix))); + 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; + } + + int entitiesCreated = 0; + 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); + } + } + } + + // Pre-allocate all Vulkan resources for this entity + if (!renderer->preAllocateEntityResources(materialEntity)) { + std::cerr << "Failed to pre-allocate resources for entity: " << entityName << std::endl; + // Continue with other entities even if one fails + } + + // Create physics body for collision with balls + // Use mesh collision shape for accurate geometry interaction + PhysicsSystem* physicsSystem = engine->GetPhysicsSystem(); + if (physicsSystem) { + // Only create a physics body if the mesh has valid geometry + auto* mc = materialEntity->GetComponent(); + if (mc && !mc->GetVertices().empty() && !mc->GetIndices().empty()) { + // Queue rigid body creation to the main thread to avoid races + physicsSystem->EnqueueRigidBodyCreation( + materialEntity, + CollisionShape::Mesh, + 0.0f, // mass 0 = static + true, // kinematic + 0.15f, // restitution + 0.5f // friction + ); + std::cout << "Queued physics body for geometry entity: " << entityName << std::endl; + } else { + std::cerr << "Skipping physics body for entity (no geometry): " << entityName << std::endl; + } + } + + entitiesCreated++; + } else { + std::cerr << "Failed to create entity for material " << materialMesh.materialName << std::endl; + } + } + } catch (const std::exception& e) { + std::cerr << "Error loading GLTF model: " << e.what() << std::endl; + } +} + +/** + * @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) { + // 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 new file mode 100644 index 00000000..332fdfa2 --- /dev/null +++ b/attachments/simple_engine/scene_loading.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include "model_loader.h" + +// Forward declarations +class Engine; +class ModelLoader; + +/** + * @brief Load a GLTF model synchronously on the main thread. + * @param engine The engine to create entities in. + * @param modelPath The path to the GLTF model file. + * @param position The position to place the model. + * @param rotation The rotation to apply to the model. + * @param scale The scale to apply to the model. + */ +void 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); diff --git a/attachments/simple_engine/shaders/hrtf.slang b/attachments/simple_engine/shaders/hrtf.slang new file mode 100644 index 00000000..f734b106 --- /dev/null +++ b/attachments/simple_engine/shaders/hrtf.slang @@ -0,0 +1,246 @@ +// Compute shader for HRTF (Head-Related Transfer Function) audio processing +// This shader processes audio data to create 3D spatial audio effects + +// Input/output buffer bindings +[[vk::binding(0, 0)]] RWStructuredBuffer inputAudioBuffer; // Raw audio samples +[[vk::binding(1, 0)]] RWStructuredBuffer outputAudioBuffer; // Processed audio samples +[[vk::binding(2, 0)]] StructuredBuffer hrtfData; // HRTF impulse responses +[[vk::binding(3, 0)]] ConstantBuffer params; // HRTF parameters + +// Parameters for HRTF processing - MUST match CPU GPUHRTFParams structure exactly +struct HRTFParams { + float4 listenerPosition; // Position of the listener (float[4] on CPU) - 16 bytes + float4 listenerForward; // Forward direction of the listener (float[4] on CPU) - 16 bytes + float4 listenerUp; // Up direction of the listener (float[4] on CPU) - 16 bytes + float4 sourcePosition; // Position of the sound source (float[4] on CPU) - 16 bytes + float sampleCount; // Number of samples to process (4 bytes) - offset 64 + float3 padding1; // Padding to align to 16-byte boundary (12 bytes) - offset 68 + uint inputChannels; // Number of input channels (4 bytes) - offset 80 + uint outputChannels; // Number of output channels (4 bytes) - offset 84 + uint hrtfSize; // Size of each HRTF impulse response (4 bytes) - offset 88 + uint numHrtfPositions; // Number of HRTF positions (4 bytes) - offset 92 + float distanceAttenuation; // Distance attenuation factor (4 bytes) - offset 96 + float dopplerFactor; // Doppler effect factor (4 bytes) - offset 100 + float reverbMix; // Reverb mix factor (4 bytes) - offset 104 + float padding2; // Padding to complete 16-byte alignment (4 bytes) - offset 108 +}; + +// Helper function to calculate the index of the closest HRTF in the dataset +uint FindClosestHRTF(float azimuth, float elevation) { + // This is a simplified implementation + // In a real implementation, this would find the closest HRTF in the dataset + // based on the azimuth and elevation angles + + // Normalize azimuth to [0, 360) degrees + azimuth = fmod(azimuth + 360.0, 360.0); + + // Clamp elevation to [-90, 90] degrees + elevation = clamp(elevation, -90.0, 90.0); + + // Calculate indices based on a typical HRTF dataset layout + // Assuming 10-degree resolution in azimuth and 15-degree in elevation + uint azimuthIndex = uint(round(azimuth / 10.0)) % 36; + uint elevationIndex = uint(round((elevation + 90.0) / 15.0)) % 13; + + // Calculate the final index + return elevationIndex * 36 + azimuthIndex; +} + +// Helper function to calculate azimuth and elevation angles +void CalculateAngles(float3 sourceDir, float3 listenerForward, float3 listenerUp, out float azimuth, out float elevation) { + // Simplified angle calculation - directly use source direction + // Calculate azimuth (horizontal angle) - angle around Y axis + azimuth = atan2(sourceDir.x, -sourceDir.z) * 57.2957795; // Convert to degrees, negate z for correct orientation + + // Calculate elevation (vertical angle) - angle from horizontal plane + float horizontalLength = sqrt(sourceDir.x * sourceDir.x + sourceDir.z * sourceDir.z); + elevation = atan2(sourceDir.y, horizontalLength) * 57.2957795; // Convert to degrees +} + +// Main compute shader function +[shader("compute")] +[numthreads(64, 1, 1)] +void main(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if the thread is within bounds + if (index >= uint(params.sampleCount)) { + return; + } + + // STAGE 1: HRTF DATA ACCESS WITH SAFETY VALIDATION + // Start with working basic panning and add HRTF data access + + // Get input sample for this thread + float inputSample = inputAudioBuffer[index]; + + // STAGE 1: Test HRTF data buffer access with ultra-safe bounds checking + bool hrtfDataValid = false; + float testHrtfSample = 0.0f; + + // Ultra-safe HRTF data access test + if (params.hrtfSize > 0 && params.numHrtfPositions > 0) { + // Test access to first HRTF sample with multiple safety checks + uint testHrtfIndex = 0; // Start with first sample + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; // 2 channels + + if (testHrtfIndex < maxHrtfBufferSize && testHrtfIndex < 500000) { // Additional hardcoded safety limit + testHrtfSample = hrtfData[testHrtfIndex]; + hrtfDataValid = true; + } + } + + // STAGE 2: 3D DIRECTION CALCULATION AND ANGLE COMPUTATION + // Calculate 3D direction from listener to source + float3 sourceDir = params.sourcePosition.xyz - params.listenerPosition.xyz; + float distance = length(sourceDir); + + // Handle edge case where listener and source are at same position + if (distance < 0.001) { + sourceDir = float3(0.0, 0.0, -1.0); // Default to front direction + distance = 1.0; + } else { + sourceDir = normalize(sourceDir); + } + + // Calculate azimuth and elevation angles using the helper function + float azimuth, elevation; + CalculateAngles(sourceDir, params.listenerForward.xyz, params.listenerUp.xyz, azimuth, elevation); + + + // ENHANCED SPATIAL PROCESSING: Use 3D angles for better panning + float leftGain = 1.0; + float rightGain = 1.0; + + // Convert azimuth to left/right panning (-180 to +180 degrees) + // Positive azimuth = right side, negative = left side + if (azimuth > 0.0) { + // Source is to the right, reduce left channel based on angle + float rightness = min(1.0, azimuth / 90.0); // Normalize to 0-1 for 0-90 degrees + leftGain = max(0.2, 1.0 - rightness * 0.8); // Reduce left by up to 80% + rightGain = 1.0; + } else if (azimuth < 0.0) { + // Source is to the left, reduce right channel based on angle + float leftness = min(1.0, -azimuth / 90.0); // Normalize to 0-1 for 0-90 degrees + leftGain = 1.0; + rightGain = max(0.2, 1.0 - leftness * 0.8); // Reduce right by up to 80% + } + + // Apply distance attenuation (closer sources are louder) + float distanceAttenuation = 1.0 / max(1.0, distance * 0.5); // Gentle distance falloff + leftGain *= distanceAttenuation; + rightGain *= distanceAttenuation; + + // STAGE 3: HRTF INDEX LOOKUP WITH BOUNDS CHECKING + // Find the closest HRTF in the dataset based on calculated angles + uint hrtfIndex = FindClosestHRTF(azimuth, elevation); + + // Ultra-safe bounds checking for HRTF index + bool hrtfIndexValid = false; + if (hrtfIndex < params.numHrtfPositions && params.numHrtfPositions > 0) { + hrtfIndexValid = true; + } + + // ENHANCED HRTF DATA ACCESS: Use calculated index instead of just first sample + float hrtfLeftSample = 0.0f; + float hrtfRightSample = 0.0f; + bool hrtfSamplesValid = false; + + if (hrtfIndexValid && hrtfDataValid) { + // Calculate HRTF buffer offsets for left and right channels + // HRTF data layout: [position0_left_samples][position0_right_samples][position1_left_samples]... + uint leftChannelOffset = hrtfIndex * params.hrtfSize * 2; // 2 channels per position + uint rightChannelOffset = leftChannelOffset + params.hrtfSize; + + // Ultra-safe bounds checking for HRTF sample access + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; + if (leftChannelOffset < maxHrtfBufferSize && rightChannelOffset < maxHrtfBufferSize && + leftChannelOffset < 500000 && rightChannelOffset < 500000) { // Additional hardcoded safety + + // Access first sample of each channel's impulse response for this position + hrtfLeftSample = hrtfData[leftChannelOffset]; + hrtfRightSample = hrtfData[rightChannelOffset]; + hrtfSamplesValid = true; + } + } + + // STAGE 4: HRTF CONVOLUTION LOOP WITH ULTRA-SAFE MEMORY ACCESS + float leftConvolution = 0.0f; + float rightConvolution = 0.0f; + uint convolutionSamples = 0; + + if (hrtfIndexValid && hrtfDataValid && params.hrtfSize > 0) { + // Calculate base offsets for this HRTF position + uint leftChannelBase = hrtfIndex * params.hrtfSize * 2; + uint rightChannelBase = leftChannelBase + params.hrtfSize; + uint maxHrtfBufferSize = params.numHrtfPositions * params.hrtfSize * 2; + + // Limit convolution size for safety and performance + uint safeHrtfSize = min(params.hrtfSize, 32u); // Limit to 32 samples for safety + + // HRTF Convolution loop with ultra-safe bounds checking + for (uint i = 0; i < safeHrtfSize; i++) { + // Check if we can access the input audio sample + if (index >= i) { + uint inputIndex = index - i; + + // Ultra-safe input buffer bounds check + if (inputIndex < uint(params.sampleCount) && inputIndex < 1024) { + float audioSample = inputAudioBuffer[inputIndex]; + + // Calculate HRTF sample indices with bounds checking + uint leftHrtfIndex = leftChannelBase + i; + uint rightHrtfIndex = rightChannelBase + i; + + // Ultra-safe HRTF buffer bounds check + if (leftHrtfIndex < maxHrtfBufferSize && rightHrtfIndex < maxHrtfBufferSize && + leftHrtfIndex < 500000 && rightHrtfIndex < 500000) { + + float leftHrtfSample = hrtfData[leftHrtfIndex]; + float rightHrtfSample = hrtfData[rightHrtfIndex]; + + // Apply convolution + leftConvolution += audioSample * leftHrtfSample; + rightConvolution += audioSample * rightHrtfSample; + convolutionSamples++; + } + } + } + } + + } + + // STAGE 4: Apply convolution results with distance attenuation + if (convolutionSamples > 0) { + // Use convolution results instead of simple gain modification + leftGain = leftConvolution * distanceAttenuation; + rightGain = rightConvolution * distanceAttenuation; + } + + + // STAGE 5: COMPLETE HRTF PROCESSING - FINAL OUTPUT WITH OPTIMIZATION + // Write to both output channels with full HRTF processing + for (uint channel = 0; channel < 2; channel++) { // Hardcode to 2 channels for safety + uint outputIndex = index * 2 + channel; + + // Ultra-safe bounds check with hardcoded limits + if (outputIndex < 1024 * 2 && outputIndex < 2048) { + float finalSample = 0.0f; + + if (convolutionSamples > 0) { + // STAGE 5: Use full HRTF convolution results + finalSample = (channel == 0) ? leftGain : rightGain; + + // Apply output normalization to prevent clipping + finalSample = clamp(finalSample, -1.0f, 1.0f); + } else { + // Fallback: Enhanced spatial panning + float channelGain = (channel == 0) ? leftGain : rightGain; + finalSample = inputSample * channelGain; + } + + outputAudioBuffer[outputIndex] = finalSample; + } + } + +} diff --git a/attachments/simple_engine/shaders/imgui.slang b/attachments/simple_engine/shaders/imgui.slang new file mode 100644 index 00000000..44541432 --- /dev/null +++ b/attachments/simple_engine/shaders/imgui.slang @@ -0,0 +1,50 @@ +// Combined vertex and fragment shader for ImGui rendering + +// Input from vertex buffer +struct VSInput { + float2 Position : POSITION; + float2 UV : TEXCOORD0; + float4 Color : COLOR0; +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float2 UV : TEXCOORD0; + float4 Color : COLOR0; +}; + +// Push constants for transformation +struct PushConstants { + float2 Scale; + float2 Translate; +}; + +// Bindings +[[vk::push_constant]] PushConstants pushConstants; +[[vk::binding(0, 0)]] Sampler2D fontTexture; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position + output.Position = float4(input.Position * pushConstants.Scale + pushConstants.Translate, 0.0, 1.0); + + // Pass UV and color to fragment shader + output.UV = input.UV; + output.Color = input.Color; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample font texture and multiply by color + float4 color = input.Color * fontTexture.Sample(input.UV); + return color; +} diff --git a/attachments/simple_engine/shaders/lighting.slang b/attachments/simple_engine/shaders/lighting.slang new file mode 100644 index 00000000..4837f3d5 --- /dev/null +++ b/attachments/simple_engine/shaders/lighting.slang @@ -0,0 +1,100 @@ +// Combined vertex and fragment shader for basic/legacy lighting +// This shader implements the Phong lighting model as a fallback when BRDF/PBR is disabled +// Note: BRDF/PBR is now the default lighting model - this is used only when explicitly requested + +// Input from vertex buffer +struct VSInput { + float3 Position : POSITION; + float3 Normal : NORMAL; + float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Added to match vertex layout (unused in basic lighting) +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos : POSITION; + float3 Normal : NORMAL; + float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Pass through tangent (unused in basic lighting) +}; + +// Uniform buffer for transformation matrices and light information +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 lightPos; + float4 lightColor; + float4 viewPos; +}; + +// Push constants for material properties +struct PushConstants { + float4 ambientColor; + float4 diffuseColor; + float4 specularColor; + float shininess; +}; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Sampler2D texSampler; + +// Push constants +[[vk::push_constant]] PushConstants material; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform position to clip space + float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position to fragment shader + output.WorldPos = worldPos.xyz; + + // Transform normal to world space + output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + + // Pass texture coordinates + output.TexCoord = input.TexCoord; + + // Pass tangent (unused in basic lighting but required for vertex layout compatibility) + output.Tangent = input.Tangent; + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample texture + float4 texColor = texSampler.Sample(input.TexCoord); + + // Normalize vectors + float3 normal = normalize(input.Normal); + float3 lightDir = normalize(ubo.lightPos.xyz - input.WorldPos); + float3 viewDir = normalize(ubo.viewPos.xyz - input.WorldPos); + float3 reflectDir = reflect(-lightDir, normal); + + // Ambient + float3 ambient = material.ambientColor.rgb * ubo.lightColor.rgb; + + // Diffuse + float diff = max(dot(normal, lightDir), 0.0); + float3 diffuse = diff * material.diffuseColor.rgb * ubo.lightColor.rgb; + + // Specular + float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + float3 specular = spec * material.specularColor.rgb * ubo.lightColor.rgb; + + // Combine components + float3 result = (ambient + diffuse + specular) * texColor.rgb; + + return float4(result, texColor.a); +} diff --git a/attachments/simple_engine/shaders/pbr.slang b/attachments/simple_engine/shaders/pbr.slang new file mode 100644 index 00000000..5d3fd564 --- /dev/null +++ b/attachments/simple_engine/shaders/pbr.slang @@ -0,0 +1,354 @@ +// Combined vertex and fragment shader for PBR rendering + +// Input from vertex buffer +struct VSInput { + [[vk::location(0)]] float3 Position; + [[vk::location(1)]] float3 Normal; + [[vk::location(2)]] float2 UV; + [[vk::location(3)]] float4 Tangent; + + // Per-instance data as true matrices + [[vk::location(4)]] column_major float4x4 InstanceModelMatrix; // binding 1 (uses 4 locations) + [[vk::location(8)]] column_major float4x3 InstanceNormalMatrix; // binding 1 (uses 3 locations) +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos; + float3 Normal : NORMAL; + float2 UV : TEXCOORD0; + float4 Tangent : TANGENT; +}; + +// Light data structure for storage buffer +// Explicit offsets ensure exact match with CPU-side layout +struct LightData { + [[vk::offset(0)]] float4 position; // Directional: direction (w=0); Point/Spot/Emissive: world position (w=1) + [[vk::offset(16)]] float4 color; // Light color and intensity + // Match GLM column-major matrices in CPU + [[vk::offset(32)]] column_major float4x4 lightSpaceMatrix; // Light space matrix for shadow mapping + [[vk::offset(96)]] int lightType; // 0=Point, 1=Directional, 2=Spot, 3=Emissive + [[vk::offset(100)]] float range; // Light range + [[vk::offset(104)]] float innerConeAngle;// For spot lights + [[vk::offset(108)]] float outerConeAngle;// For spot lights +}; + +// 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; // Number of active lights (dynamic) + int padding0; // Padding for alignment + float padding1; // Padding for alignment + float padding2; // Padding for alignment +}; + +// 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; // Emissive factor for HDR emissive sources + float emissiveStrength; // KHR_materials_emissive_strength extension + float transmissionFactor; // KHR_materials_transmission + int useSpecGlossWorkflow; // 1 if using KHR_materials_pbrSpecularGlossiness + float glossinessFactor; // SpecGloss glossiness scalar + float3 specularFactor; // SpecGloss specular color factor +}; + +// Constants +static const float PI = 3.14159265359; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Sampler2D baseColorMap; +[[vk::binding(2, 0)]] Sampler2D metallicRoughnessMap; +[[vk::binding(3, 0)]] Sampler2D normalMap; +[[vk::binding(4, 0)]] Sampler2D occlusionMap; +[[vk::binding(5, 0)]] Sampler2D emissiveMap; +[[vk::binding(6, 0)]] StructuredBuffer lightBuffer; // Dynamic light storage buffer + +[[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 / denom; +} + +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(1.0 - cosTheta, 5.0); +} + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Use instance matrices directly + float4x4 instanceModelMatrix = input.InstanceModelMatrix; + float3x3 instanceNormalMatrix3x3 = (float3x3)input.InstanceNormalMatrix; + + // Transform position to world space: entity model * instance model + float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position to fragment shader + output.WorldPos = worldPos.xyz; + + // Transform normal and tangent to world space (apply instance normal then entity model) + float3x3 model3x3 = (float3x3)ubo.model; + output.Normal = normalize(mul(model3x3, mul(instanceNormalMatrix3x3, input.Normal))); + + float3 instTangent = mul(instanceNormalMatrix3x3, input.Tangent.xyz); + float3 worldTangent = normalize(mul(model3x3, instTangent)); + + // Pass texture coordinates + output.UV = input.UV; + + // Pass world-space tangent (preserve handedness in w) + output.Tangent = float4(worldTangent, input.Tangent.w); + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample material textures (flip V to match glTF UV origin) + float2 uv = float2(input.UV.x, 1.0 - input.UV.y); + float4 baseColor = baseColorMap.Sample(uv) * material.baseColorFactor; + // For MR workflow: metallic in B, roughness in G; For SpecGloss: RGB=specular color, A=glossiness + float4 mrOrSpecGloss = (material.physicalDescriptorTextureSet < 0) ? float4(1.0, 1.0, 1.0, 1.0) : metallicRoughnessMap.Sample(uv); + float metallic = 0.0; + float roughness = 1.0; + float3 specColorSG = float3(0.0, 0.0, 0.0); + if (material.useSpecGlossWorkflow != 0) { + // Specular-Glossiness workflow + 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); + metallic = 0.0; // not used in SpecGloss + } else { + // Metallic-Roughness workflow + float2 metallicRoughness = mrOrSpecGloss.bg; + metallic = clamp(metallicRoughness.x * material.metallicFactor, 0.0, 1.0); + roughness = clamp(metallicRoughness.y * material.roughnessFactor, 0.0, 1.0); + } + float ao = occlusionMap.Sample(uv).r; + float3 emissiveTex = (material.emissiveTextureSet < 0) ? float3(1.0, 1.0, 1.0) : emissiveMap.Sample(uv).rgb; + float3 emissive = emissiveTex * material.emissiveFactor * material.emissiveStrength; + + // Early alpha masking discard for masked materials only (glTF alphaMode == MASK) + if (material.alphaMask > 0.5 && baseColor.a < material.alphaMaskCutoff) { + discard; + } + + // Calculate normal in tangent space + float3 N = normalize(input.Normal); + if (material.normalTextureSet >= 0) { + // Apply normal mapping + float3 tangentNormal = normalMap.Sample(uv).xyz * 2.0 - 1.0; + + float3 T = input.Tangent.xyz; + bool hasTangent = dot(T, T) > 1e-6; + if (hasTangent) { + // Orthonormalize T against N to reduce shading artifacts + T = normalize(T); + T = normalize(T - N * dot(N, T)); + } else { + // Fallback: derive tangent from screen-space derivatives of position and UVs + float3 dp1 = ddx(input.WorldPos); + float3 dp2 = ddy(input.WorldPos); + float2 duv1 = ddx(uv); + float2 duv2 = ddy(uv); + float det = duv1.x * duv2.y - duv1.y * duv2.x; + if (abs(det) > 1e-8) { + float r = 1.0 / det; + T = normalize((dp1 * duv2.y - dp2 * duv1.y) * r); + } else { + // Degenerate UV derivatives; fall back to a stable orthogonal vector + float3 up = (abs(N.y) < 0.999) ? float3(0.0, 1.0, 0.0) : float3(1.0, 0.0, 0.0); + T = normalize(cross(up, N)); + } + } + float handedness = hasTangent ? input.Tangent.w : 1.0; + float3 B = normalize(cross(N, T)) * handedness; + // Construct tangent-to-world with T, B, N basis + float3x3 TBN = float3x3(T, B, N); + // Transform from tangent to world space using column-vector convention + N = normalize(mul(TBN, tangentNormal)); + } + + // Calculate view and reflection vectors + float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + float3 R = reflect(-V, N); + + // Calculate F0 (base reflectivity) + float3 F0; + if (material.useSpecGlossWorkflow != 0) { + // SpecGloss: use specular color directly + F0 = specColorSG; + } else { + // MR: interpolate between dielectric and baseColor by metallic + F0 = float3(0.04, 0.04, 0.04); + F0 = lerp(F0, baseColor.rgb, metallic); + } + + // Initialize lighting + float3 Lo = float3(0.0, 0.0, 0.0); + + // Calculate lighting for each light (dynamic count - no limit) + for (int i = 0; i < ubo.lightCount; i++) { + LightData light = lightBuffer[i]; + float3 lightVec = light.position.xyz; // w=0 indicates direction (directional), w=1 indicates position (point/spot/emissive) + float3 lightColor = light.color.rgb; + + // Handle emissive lights differently + if (light.lightType == 3) { // Emissive light + // Treat emissive like a positional contributor from its stored position + float3 L = normalize(lightVec - input.WorldPos); + float distance = length(lightVec - input.WorldPos); + float attenuation = 1.0 / (distance * distance); + float3 radiance = lightColor * attenuation; + + float3 H = normalize(V + L); + + float NdotL = max(dot(N, L), 0.0); + 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 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + + } else if (light.lightType == 1) { // Directional light + // For directional lights, position field stores direction; use no distance attenuation + float3 L = normalize(-lightVec); // light direction towards the surface + float3 radiance = lightColor; // No attenuation with distance + + float3 H = normalize(V + L); + + float NdotL = max(dot(N, L), 0.0); + 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 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + + } else { // Point/Spot lights + float3 L = normalize(lightVec - input.WorldPos); + float distance = length(lightVec - input.WorldPos); + float attenuation = 1.0 / (distance * distance); + float3 radiance = lightColor * attenuation; + + float3 H = normalize(V + L); + + float NdotL = max(dot(N, L), 0.0); + 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 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + } + } + + float3 ambient = baseColor.rgb * ao * (0.03 * ubo.scaleIBLAmbient); + // Base lit color from direct lighting and emissive + float3 opaqueLit = ambient + Lo + emissive; + + // Transmission respecting roughness and Fresnel, but without environment map + float T = clamp(material.transmissionFactor, 0.0, 1.0); + float NdotV_glass = max(dot(N, V), 0.0); + float3 Fv = FresnelSchlick(NdotV_glass, F0); + float Favg = (Fv.x + Fv.y + Fv.z) / 3.0; + float roughTrans = clamp(1.0 - (roughness * roughness), 0.0, 1.0); + float T_eff = T * (1.0 - Favg) * roughTrans; + + // Energy-conserving mix between opaque lighting and transmitted base color tint + float3 color = lerp(opaqueLit, baseColor.rgb, T_eff); + + // Apply exposure before tone mapping for proper HDR workflow + color *= ubo.exposure; + + // Standard Reinhard tone mapping - simple and effective + color = color / (1.0 + color); + + // Gamma correction (convert from linear to sRGB) + color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + + // Alpha approximates remaining opacity (higher transmission -> lower alpha), clamped for readability + float alphaOut = baseColor.a; + if (T > 0.001) { + alphaOut = clamp(1.0 - T_eff, 0.08, 0.60); + } + return float4(color, alphaOut); +} diff --git a/attachments/simple_engine/shaders/physics.slang b/attachments/simple_engine/shaders/physics.slang new file mode 100644 index 00000000..82822970 --- /dev/null +++ b/attachments/simple_engine/shaders/physics.slang @@ -0,0 +1,444 @@ +// Compute shader for physics simulation +// This shader processes rigid body physics data to simulate physical interactions + + +// Physics data structure +struct PhysicsData { + float4 position; // xyz = position, w = inverse mass + float4 rotation; // quaternion + float4 linearVelocity; // xyz = velocity, w = restitution + float4 angularVelocity; // xyz = angular velocity, w = friction + float4 force; // xyz = force, w = is kinematic (0 or 1) + float4 torque; // xyz = torque, w = use gravity (0 or 1) + float4 colliderData; // type-specific data (e.g., radius for spheres) + float4 colliderData2; // additional collider data (e.g., box half extents) +}; + +// Collision data structure +struct CollisionData { + uint bodyA; + uint bodyB; + float4 contactNormal; // xyz = normal, w = penetration depth + float4 contactPoint; // xyz = contact point, w = unused +}; + +// Input/output buffer bindings +[[vk::binding(0, 0)]] RWStructuredBuffer physicsBuffer; // Physics data +[[vk::binding(1, 0)]] RWStructuredBuffer collisionBuffer; // Collision data +[[vk::binding(2, 0)]] RWStructuredBuffer pairBuffer; // Potential collision pairs +[[vk::binding(3, 0)]] RWStructuredBuffer counterBuffer; // [0] = pair count, [1] = collision count + +// Parameters for physics simulation +[[vk::binding(4, 0)]] ConstantBuffer params; + +struct PhysicsParams { + float deltaTime; // Time step - 4 bytes + uint numBodies; // Number of rigid bodies - 4 bytes + uint maxCollisions; // Maximum number of collisions - 4 bytes + float padding; // Explicit padding to align gravity to 16-byte boundary - 4 bytes + float4 gravity; // Gravity vector (xyz) + padding (w) - 16 bytes + // Total: 32 bytes (aligned to 16-byte boundaries for std140 layout) +}; + +// Quaternion multiplication +float4 quatMul(float4 q1, float4 q2) { + return float4( + q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, + q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, + q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, + q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + ); +} + +// Quaternion normalization +float4 quatNormalize(float4 q) { + float len = length(q); + if (len > 0.0001) { + return q / len; + } + return float4(0, 0, 0, 1); +} + +// Integration shader - updates positions and velocities +[shader("compute")] +[numthreads(64, 1, 1)] +void IntegrateCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of bodies + if (index >= params.numBodies) { + return; + } + + // Get physics data for this body + PhysicsData body = physicsBuffer[index]; + + + // Skip kinematic bodies + if (body.force.w > 0.5) { + return; + } + + // Apply gravity if enabled + if (body.torque.w > 0.5) { + float3 gravityForce = params.gravity.xyz * body.position.w; + body.force.xyz += gravityForce; + + } + + // Integrate forces + float3 velocityChange = body.force.xyz * body.position.w * params.deltaTime; + body.linearVelocity.xyz += velocityChange; + body.angularVelocity.xyz += body.torque.xyz * params.deltaTime; // Simplified, should use inertia tensor + + + // Apply damping + const float linearDamping = 0.01; + const float angularDamping = 0.01; + body.linearVelocity.xyz *= (1.0 - linearDamping); + body.angularVelocity.xyz *= (1.0 - angularDamping); + + // Integrate velocities + float3 positionChange = body.linearVelocity.xyz * params.deltaTime; + body.position.xyz += positionChange; + + + // Update rotation + float4 angularVelocityQuat = float4(body.angularVelocity.xyz * 0.5, 0.0); + float4 rotationDelta = quatMul(angularVelocityQuat, body.rotation); + body.rotation = quatNormalize(body.rotation + rotationDelta * params.deltaTime); + + // Write updated data back to buffer + physicsBuffer[index] = body; + +} + +// Compute AABB for a body +void computeAABB(PhysicsData body, out float3 min, out float3 max) { + // Default to a small AABB + min = body.position.xyz - float3(0.1, 0.1, 0.1); + max = body.position.xyz + float3(0.1, 0.1, 0.1); + + // Check collider type + int colliderType = int(body.colliderData.w); + + if (colliderType == 0) { // Sphere + float radius = body.colliderData.x; + float3 center = body.position.xyz + body.colliderData2.xyz; + min = center - float3(radius, radius, radius); + max = center + float3(radius, radius, radius); + } + else if (colliderType == 1) { // Box + float3 halfExtents = body.colliderData.xyz; + float3 center = body.position.xyz + body.colliderData2.xyz; + // This is simplified - should account for rotation + min = center - halfExtents; + max = center + halfExtents; + } + else if (colliderType == 2) { // Mesh (represented as large bounding box) + float3 halfExtents = body.colliderData.xyz; + float3 center = body.position.xyz + body.colliderData2.xyz; + // This is simplified - should account for rotation + min = center - halfExtents; + max = center + halfExtents; + } +} + +// Check if two AABBs overlap +bool aabbOverlap(float3 minA, float3 maxA, float3 minB, float3 maxB) { + return all(minA < maxB) && all(minB < maxA); +} + +// Broad phase collision detection - identifies potential collision pairs +[shader("compute")] +[numthreads(64, 1, 1)] +void BroadPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Calculate total number of pairs + uint numPairs = (params.numBodies * (params.numBodies - 1)) / 2; + + if (index >= numPairs) { + return; + } + + // Convert linear index to pair indices (i, j) where i < j + // Use a more robust algorithm that avoids floating-point precision issues + uint i = 0; + uint j = 0; + + // Find i and j using integer arithmetic to avoid precision errors + uint remaining = index; + uint currentRow = 0; + + // Find which row (i value) this index belongs to + while (remaining >= (params.numBodies - 1 - currentRow)) { + remaining -= (params.numBodies - 1 - currentRow); + currentRow++; + } + + i = currentRow; + j = i + 1 + remaining; + + // Get physics data for both bodies + PhysicsData bodyA = physicsBuffer[i]; + PhysicsData bodyB = physicsBuffer[j]; + + + // Skip if both bodies are kinematic + if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { + return; + } + + // Skip if either body doesn't have a collider + if (bodyA.colliderData.w < 0 || bodyB.colliderData.w < 0) { + return; + } + + // Early culling: only consider pairs where at least one body is a sphere (shape 0) + int shapeA = int(bodyA.colliderData.w); + int shapeB = int(bodyB.colliderData.w); + if (!(shapeA == 0 || shapeB == 0)) { + return; + } + + // Compute AABBs + float3 minA, maxA, minB, maxB; + computeAABB(bodyA, minA, maxA); + computeAABB(bodyB, minB, maxB); + + // Expand sphere AABBs by motion over the timestep to catch fast-moving spheres + if (shapeA == 0) { + float3 expandA = abs(bodyA.linearVelocity.xyz) * params.deltaTime; + minA -= expandA; maxA += expandA; + } + if (shapeB == 0) { + float3 expandB = abs(bodyB.linearVelocity.xyz) * params.deltaTime; + minB -= expandB; maxB += expandB; + } + + // Check for AABB overlap + if (aabbOverlap(minA, maxA, minB, maxB)) { + // Add to potential collision pairs + uint pairIndex; + InterlockedAdd(counterBuffer[0], 1, pairIndex); + + if (pairIndex < params.maxCollisions) { + pairBuffer[pairIndex] = uint2(i, j); + } + } +} + +// Narrow phase collision detection - detailed collision detection for potential pairs +[shader("compute")] +[numthreads(64, 1, 1)] +void NarrowPhaseCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of potential pairs + uint numPairs = counterBuffer[0]; + if (index >= numPairs || index >= params.maxCollisions) { + return; + } + + // Get the pair of bodies + uint2 pair = pairBuffer[index]; + uint bodyIndexA = pair.x; + uint bodyIndexB = pair.y; + + PhysicsData bodyA = physicsBuffer[bodyIndexA]; + PhysicsData bodyB = physicsBuffer[bodyIndexB]; + + // Determine collision shapes + int shapeA = int(bodyA.colliderData.w); + int shapeB = int(bodyB.colliderData.w); + + // Handle sphere-sphere collisions + if (shapeA == 0 && shapeB == 0) { // Both are spheres + float radiusA = bodyA.colliderData.x; + float radiusB = bodyB.colliderData.x; + + float3 posA = bodyA.position.xyz + bodyA.colliderData2.xyz; + float3 posB = bodyB.position.xyz + bodyB.colliderData2.xyz; + + float3 direction = posB - posA; + float distance = length(direction); + float minDistance = radiusA + radiusB; + + if (distance < minDistance) { + // Collision detected + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + + if (collisionIndex < params.maxCollisions) { + // Normalize direction + float3 normal = direction / max(distance, 0.0001); + + // Create collision data + CollisionData collision; + collision.bodyA = bodyIndexA; + collision.bodyB = bodyIndexB; + collision.contactNormal = float4(normal, minDistance - distance); // penetration depth + collision.contactPoint = float4(posA + normal * radiusA, 0); + + // Store collision data + collisionBuffer[collisionIndex] = collision; + } + } + } + // Handle sphere-geometry collisions (sphere vs mesh represented as box) + else if ((shapeA == 0 && shapeB == 2) || (shapeA == 2 && shapeB == 0)) { + // Determine which is sphere and which is geometry + PhysicsData sphere = (shapeA == 0) ? bodyA : bodyB; + PhysicsData geometry = (shapeA == 0) ? bodyB : bodyA; + uint sphereIndex = (shapeA == 0) ? bodyIndexA : bodyIndexB; + uint geometryIndex = (shapeA == 0) ? bodyIndexB : bodyIndexA; + + float sphereRadius = sphere.colliderData.x; + float3 spherePos = sphere.position.xyz + sphere.colliderData2.xyz; + float3 geometryPos = geometry.position.xyz + geometry.colliderData2.xyz; + float3 geometryHalfExtents = geometry.colliderData.xyz; + + // Simple sphere-box collision detection + float3 closestPoint = clamp(spherePos, geometryPos - geometryHalfExtents, geometryPos + geometryHalfExtents); + float3 direction = spherePos - closestPoint; + float distance = length(direction); + + if (distance < sphereRadius) { + // Collision detected (overlap) + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + + if (collisionIndex < params.maxCollisions) { + // Calculate normal so that it points from sphere(A) to geometry(B) + float3 normal = (distance > 0.0001) ? (-direction / distance) : float3(0, -1, 0); + float penetration = sphereRadius - distance; + + // Create collision data + CollisionData collision; + collision.bodyA = sphereIndex; + collision.bodyB = geometryIndex; + collision.contactNormal = float4(normal, penetration); + collision.contactPoint = float4(closestPoint, 0); + + // Store collision data + collisionBuffer[collisionIndex] = collision; + } + } else { + // Swept test (CCD-lite): segment from previous position to current against box expanded by sphere radius + float3 prevPos = spherePos - sphere.linearVelocity.xyz * params.deltaTime; + float3 dir = spherePos - prevPos; + float dirLen = length(dir); + if (dirLen > 1e-6) { + float3 bbMin = geometryPos - (geometryHalfExtents + sphereRadius); + float3 bbMax = geometryPos + (geometryHalfExtents + sphereRadius); + + float3 invDir = 1.0 / max(abs(dir), float3(1e-6, 1e-6, 1e-6)); + float3 t0 = (bbMin - prevPos) / dir; + float3 t1 = (bbMax - prevPos) / dir; + float3 tmin3 = min(t0, t1); + float3 tmax3 = max(t0, t1); + float tEnter = max(tmin3.x, max(tmin3.y, tmin3.z)); + float tExit = min(tmax3.x, min(tmax3.y, tmax3.z)); + + if (tEnter >= 0.0 && tEnter <= 1.0 && tEnter <= tExit) { + // Determine contact normal based on entry axis and direction of motion + float3 normal = float3(0,0,0); + if (tEnter >= tmin3.x && tEnter >= tmin3.y && tEnter >= tmin3.z) { + normal = float3((dir.x > 0.0) ? 1.0 : -1.0, 0.0, 0.0); + } else if (tEnter >= tmin3.y && tEnter >= tmin3.z) { + normal = float3(0.0, (dir.y > 0.0) ? 1.0 : -1.0, 0.0); + } else { + normal = float3(0.0, 0.0, (dir.z > 0.0) ? 1.0 : -1.0); + } + + float3 hitPoint = prevPos + dir * tEnter; + + uint collisionIndex; + InterlockedAdd(counterBuffer[1], 1, collisionIndex); + if (collisionIndex < params.maxCollisions) { + CollisionData collision; + collision.bodyA = sphereIndex; + collision.bodyB = geometryIndex; + // Tiny penetration to trigger resolution without large positional correction + collision.contactNormal = float4(normalize(normal), 0.0); + collision.contactPoint = float4(hitPoint, 0.0); + collisionBuffer[collisionIndex] = collision; + } + } + } + } + } +} + +// Collision resolution - resolves detected collisions +[shader("compute")] +[numthreads(64, 1, 1)] +void ResolveCS(uint3 dispatchThreadID : SV_DispatchThreadID) { + uint index = dispatchThreadID.x; + + // Check if this thread is within the number of collisions + uint numCollisions = counterBuffer[1]; + if (index >= numCollisions || index >= params.maxCollisions) { + return; + } + + // Get collision data + CollisionData collision = collisionBuffer[index]; + + // Get the bodies involved in the collision + PhysicsData bodyA = physicsBuffer[collision.bodyA]; + PhysicsData bodyB = physicsBuffer[collision.bodyB]; + + // Skip if both bodies are kinematic + if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { + return; + } + + // Calculate relative velocity + float3 relativeVelocity = bodyB.linearVelocity.xyz - bodyA.linearVelocity.xyz; + + // Calculate velocity along normal + float velocityAlongNormal = dot(relativeVelocity, collision.contactNormal.xyz); + + // Don't resolve if velocities are separating + if (velocityAlongNormal > 0) { + return; + } + + // Calculate restitution (bounciness) + float restitution = min(bodyA.linearVelocity.w, bodyB.linearVelocity.w); + + // Calculate impulse scalar + float j = -(1.0 + restitution) * velocityAlongNormal; + j /= bodyA.position.w + bodyB.position.w; + + // Apply impulse + float3 impulse = collision.contactNormal.xyz * j; + + // Update velocities + if (bodyA.force.w < 0.5) { // If not kinematic + bodyA.linearVelocity.xyz -= impulse * bodyA.position.w; + physicsBuffer[collision.bodyA] = bodyA; + } + + if (bodyB.force.w < 0.5) { // If not kinematic + bodyB.linearVelocity.xyz += impulse * bodyB.position.w; + physicsBuffer[collision.bodyB] = bodyB; + } + + // Position correction to prevent sinking + const float percent = 0.2; // usually 20% to 80% + const float slop = 0.01; // small penetration allowed + float3 correction = max(collision.contactNormal.w - slop, 0.0) * percent * collision.contactNormal.xyz / (bodyA.position.w + bodyB.position.w); + + if (bodyA.force.w < 0.5) { // If not kinematic + bodyA.position.xyz -= correction * bodyA.position.w; + physicsBuffer[collision.bodyA] = bodyA; + } + + if (bodyB.force.w < 0.5) { // If not kinematic + bodyB.position.xyz += correction * bodyB.position.w; + physicsBuffer[collision.bodyB] = bodyB; + } +} diff --git a/attachments/simple_engine/shaders/texturedMesh.slang b/attachments/simple_engine/shaders/texturedMesh.slang new file mode 100644 index 00000000..a32f34cc --- /dev/null +++ b/attachments/simple_engine/shaders/texturedMesh.slang @@ -0,0 +1,82 @@ +// Combined vertex and fragment shader for textured mesh rendering +// This shader provides basic textured rendering with simple lighting + +// Input from vertex buffer +struct VSInput { + [[vk::location(0)]] float3 Position; + [[vk::location(1)]] float3 Normal; + [[vk::location(2)]] float2 TexCoord; + [[vk::location(3)]] float4 Tangent; + + // Per-instance data as true matrices; occupy locations 4..7 and 8..10 respectively + [[vk::location(4)]] column_major float4x4 InstanceModelMatrix; // binding 1 (consumes 4 locations) + [[vk::location(8)]] column_major float4x3 InstanceNormalMatrix; // binding 1 (consumes 3 locations) +}; + +// Output from vertex shader / Input to fragment shader +struct VSOutput { + float4 Position : SV_POSITION; + float3 WorldPos; + float3 Normal : NORMAL; + float2 TexCoord : TEXCOORD0; + float4 Tangent : TANGENT; // Pass through tangent to satisfy validation layer +}; + +// Uniform buffer +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; +}; + +// Bindings +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Sampler2D texSampler; + +// Vertex shader entry point +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Use instance matrices directly (CPU uploads column-major matrices in attributes 4..10) + float4x4 instanceModelMatrix = input.InstanceModelMatrix; + float3x3 normalMatrix3x3 = (float3x3)input.InstanceNormalMatrix; + + // Transform position to world space: entity model * instance model + float4 worldPos = mul(ubo.model, mul(instanceModelMatrix, float4(input.Position, 1.0))); + + // Final clip space position + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position and transformed normal to fragment shader (apply entity model to normals too) + float3x3 model3x3 = (float3x3)ubo.model; + output.WorldPos = worldPos.xyz; + output.Normal = normalize(mul(model3x3, mul(normalMatrix3x3, input.Normal))); + output.TexCoord = input.TexCoord; + output.Tangent = input.Tangent; // Pass through tangent (unused in basic rendering) + + return output; +} + +// Fragment shader entry point +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // Sample the texture with flipped V coordinate (glTF UV origin vs Vulkan) + float2 uv = float2(input.TexCoord.x, 1.0 - input.TexCoord.y); + float4 texColor = texSampler.Sample(uv); + + // Simple directional lighting + float3 lightDir = normalize(float3(0.5, 1.0, 0.3)); // Fixed light direction + float3 normal = normalize(input.Normal); + float lightIntensity = max(dot(normal, lightDir), 0.2); // Minimum ambient of 0.2 + + // If texture is nearly white, use a default color to avoid washed-out look + float whiteness = (texColor.r + texColor.g + texColor.b) / 3.0; + float4 finalColor = (whiteness > 0.95) + ? float4(float3(0.8, 0.8, 0.8) * lightIntensity, 1.0) + : float4(texColor.rgb * lightIntensity, texColor.a); + + return finalColor; +} diff --git a/attachments/simple_engine/swap_chain.h b/attachments/simple_engine/swap_chain.h new file mode 100644 index 00000000..0baeda21 --- /dev/null +++ b/attachments/simple_engine/swap_chain.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include + +#include "vulkan_device.h" +#include "platform.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); +}; diff --git a/attachments/simple_engine/transform_component.cpp b/attachments/simple_engine/transform_component.cpp new file mode 100644 index 00000000..b25ffae5 --- /dev/null +++ b/attachments/simple_engine/transform_component.cpp @@ -0,0 +1,31 @@ +#include "transform_component.h" + +// Most of the TransformComponent class implementation is in the header file +// This file is mainly for any methods that might need additional implementation +// +// This implementation corresponds to the Camera_Transformations chapter in the tutorial: +// @see en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc#model-matrix + +// 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; +} + +// 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; +} diff --git a/attachments/simple_engine/transform_component.h b/attachments/simple_engine/transform_component.h new file mode 100644 index 00000000..fa12050a --- /dev/null +++ b/attachments/simple_engine/transform_component.h @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include + +#include "component.h" + +/** + * @brief Component that handles the position, rotation, and scale of an entity. + * + * 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 : 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 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/vcpkg.json b/attachments/simple_engine/vcpkg.json new file mode 100644 index 00000000..d246de0d --- /dev/null +++ b/attachments/simple_engine/vcpkg.json @@ -0,0 +1,12 @@ +{ + "name": "vulkan-game-engine-tutorial", + "version": "1.0.0", + "dependencies": [ + "glfw3", + "glm", + "openal-soft", + "ktx", + "tinygltf", + "nlohmann-json" + ] +} diff --git a/attachments/simple_engine/vulkan_device.cpp b/attachments/simple_engine/vulkan_device.cpp new file mode 100644 index 00000000..bd6d9136 --- /dev/null +++ b/attachments/simple_engine/vulkan_device.cpp @@ -0,0 +1,287 @@ +#include "vulkan_device.h" +#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()); +} + +// Destructor +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; + } + + 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 = { + 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; + + // Enable Vulkan 1.3 features + vk::PhysicalDeviceVulkan13Features vulkan13Features; + vulkan13Features.dynamicRendering = vk::True; + vulkan13Features.synchronization2 = vk::True; + features.pNext = &vulkan13Features; + + // Create device + vk::DeviceCreateInfo createInfo{ + .pNext = &features, + .queueCreateInfoCount = static_cast(queueCreateInfos.size()), + .pQueueCreateInfos = queueCreateInfos.data(), + .enabledLayerCount = 0, + .ppEnabledLayerNames = nullptr, + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data(), + .pEnabledFeatures = nullptr // Using pNext for features + }; + + // Enable validation layers if requested + if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + } + + // 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; +} + +// Query swap chain support +SwapChainSupportDetails VulkanDevice::querySwapChainSupport(vk::raii::PhysicalDevice& device) { + SwapChainSupportDetails details; + + // Get surface capabilities + details.capabilities = device.getSurfaceCapabilitiesKHR(surface); + + // Get surface formats + details.formats = device.getSurfaceFormatsKHR(surface); + + // Get present modes + details.presentModes = device.getSurfacePresentModesKHR(surface); + + return details; +} + +// Check device extension support +bool 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.find(extension.extensionName) != optionalExtensionsSet.end()) { + std::cout << " " << extension.extensionName << " (supported)" << std::endl; + } + } + + 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; +} + +// 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"); +} diff --git a/attachments/simple_engine/vulkan_device.h b/attachments/simple_engine/vulkan_device.h new file mode 100644 index 00000000..8d5b55db --- /dev/null +++ b/attachments/simple_engine/vulkan_device.h @@ -0,0 +1,148 @@ +#pragma once + +#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(); + } +}; + +/** + * @brief Structure for swap chain support details. + */ +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); +}; diff --git a/attachments/simple_engine/vulkan_dispatch.cpp b/attachments/simple_engine/vulkan_dispatch.cpp new file mode 100644 index 00000000..daa4a73e --- /dev/null +++ b/attachments/simple_engine/vulkan_dispatch.cpp @@ -0,0 +1,6 @@ +#include + +// Define the defaultDispatchLoaderDynamic variable +namespace vk::detail { + DispatchLoaderDynamic defaultDispatchLoaderDynamic; +} diff --git a/en/01_Overview.adoc b/en/01_Overview.adoc index f3f394be..33493847 100644 --- a/en/01_Overview.adoc +++ b/en/01_Overview.adoc @@ -28,7 +28,7 @@ with the existing APIs somehow. This resulted in less than ideal abstractions Aside from these new features, the past decade also saw an influx of mobile and embedded devices with powerful graphics hardware. These mobile GPUs have different architectures based on their energy and space requirements. -One such example is https://en.wikipedia.org/wiki/Tiled_rendering[tiled rendering], +One such example is https://en.wikipedia.org/wiki/Tiled_rendering[tiled rendering], which would benefit from improved performance by offering the programmer more control over this functionality. Another limitation originating from the age of these APIs is limited @@ -320,7 +320,7 @@ _validation layers_. Validation layers are pieces of code that can be inserted between the API and the graphics driver to do things like running extra checks on function parameters and tracking memory management problems. -The nice thing is that you can enable them during development and then +An important benefit is that you can enable them during development and then completely disable them when releasing your application for zero overhead. Anyone can write their own validation layers, but the Vulkan SDK by LunarG provides a standard set of validation layers that we'll be using in this tutorial. diff --git a/en/02_Development_environment.adoc b/en/02_Development_environment.adoc index 19e76693..85593354 100644 --- a/en/02_Development_environment.adoc +++ b/en/02_Development_environment.adoc @@ -65,9 +65,9 @@ contains the libraries. Lastly, there's the `include` directory that contains the Vulkan headers. Feel free to explore the other files, but we won't need them for this tutorial. -To automatically set the environment variables up that VulkanSDK will use to -make life easier with the CMake project configuration and various other -tooling, We recommend using the `setup-env` script. This can be added to +To automatically set the environment variables that VulkanSDK will use, we +recommend using the `setup-env` script. This makes life easier with the CMake +project configuration and various other tooling. The script can be added to your auto-start for your terminal and IDE setup such that those environment variables work everywhere. @@ -154,9 +154,9 @@ target_sources(VulkanCppModule ) ---- -The VulkanCppModule target only needs to be defined once, then add it to the -dependency of your consuming project, and it will be built automatically, and -you won't need to also add Vulkan::Vulkan to your project. +The VulkanCppModule target only needs to be defined once. Then add it to the +dependency of your consuming project, and it will be built automatically. +You won't need to also add Vulkan::Vulkan to your project. [,cmake] ---- @@ -245,7 +245,7 @@ GLFW on the https://www.glfw.org/download.html[official website]. In this tutorial, we'll be using the 64-bit binaries, but you can of course also choose to build in 32-bit mode. In that case make sure to link with the Vulkan SDK binaries in the `Lib32` directory instead of `Lib`. After downloading it, extract the archive -to a convenient location. I've chosen to create a `Libraries` directory in the +to a convenient location. We've chosen to create a `Libraries` directory in the Visual Studio directory under documents. image::/images/glfw_directory.png[] @@ -272,7 +272,7 @@ Now that you have installed all the dependencies, we can set up a basic CMake project for Vulkan and write a little bit of code to make sure that everything works. -I will assume that you already have some basic experience with CMake, like +We will assume that you already have some basic experience with CMake, like how variables and rules work. If not, you can get up to speed very quickly with https://cmake.org/cmake/help/book/mastering-cmake/cmake/Help/guide/tutorial/[this tutorial]. You can now use the link:/attachments/[attachments] directory in this tutorial @@ -410,7 +410,7 @@ Now that you have installed all the dependencies, we can set up a basic CMake project for Vulkan and write a little bit of code to make sure that everything works. -I will assume that you already have some basic experience with CMake, like +We will assume that you already have some basic experience with CMake, like how variables and rules work. If not, you can get up to speed very quickly with https://cmake.org/cmake/help/book/mastering-cmake/cmake/Help/guide/tutorial/[this tutorial]. You can now use the link:/attachments/[attachments] directory in this tutorial as a template for your diff --git a/en/Building_a_Simple_Engine/Appendix/appendix.adoc b/en/Building_a_Simple_Engine/Appendix/appendix.adoc new file mode 100644 index 00000000..e18fb6cc --- /dev/null +++ b/en/Building_a_Simple_Engine/Appendix/appendix.adoc @@ -0,0 +1,339 @@ +:pp: {plus}{plus} + += Appendix: + +== Detailed Architectural Patterns + +This appendix provides in-depth information about common architectural patterns used in modern rendering and game engines. These patterns are referenced in the main Engine Architecture section, with a focus on Component-Based Architecture in the main tutorial. + +[[layered-architecture]] +=== Layered Architecture + +One of the most fundamental architectural patterns is the layered architecture, where the system is divided into distinct layers, each with a specific responsibility. + +image::../../../images/layered_architecture_diagram.png[Layered Architecture Diagram, width=600] + +==== Typical Layers in a Rendering Engine + +1. *Platform Abstraction Layer* - Provides a consistent interface to platform-specific functionality. +2. *Resource Management Layer* - Manages loading, caching, and unloading of assets. +3. *Rendering Layer* - Handles the rendering pipeline, shaders, and graphics API interaction. +4. *Scene Management Layer* - Manages the scene graph, spatial partitioning, and culling. +5. *Application Layer* - Handles user input, game logic, and high-level application flow. + +==== Benefits of Layered Architecture + +* Clear separation of concerns +* Easier to understand and maintain +* Can replace or modify individual layers without affecting others +* Facilitates testing of individual layers + +==== Implementation Example + +[source,cpp] +---- +// Platform Abstraction Layer +class Platform { +public: + virtual void Initialize() = 0; + virtual void* CreateWindow(int width, int height) = 0; + virtual void ProcessEvents() = 0; + // ... +}; + +// Resource Management Layer +class ResourceManager { +public: + virtual Texture* LoadTexture(const std::string& path) = 0; + virtual Mesh* LoadMesh(const std::string& path) = 0; + // ... +}; + +// Rendering Layer +class Renderer { +public: + virtual void Initialize(Platform* platform) = 0; + virtual void RenderScene(Scene* scene) = 0; + // ... +}; + +// Scene Management Layer +class SceneManager { +public: + virtual void AddEntity(Entity* entity) = 0; + virtual void UpdateScene(float deltaTime) = 0; + // ... +}; + +// Application Layer +class Application { +private: + Platform* platform; + ResourceManager* resourceManager; + Renderer* renderer; + SceneManager* sceneManager; + +public: + void Run() { + platform->Initialize(); + renderer->Initialize(platform); + + // Main loop + while (running) { + platform->ProcessEvents(); + sceneManager->UpdateScene(deltaTime); + renderer->RenderScene(sceneManager->GetActiveScene()); + } + } +}; +---- + +[[data-oriented-design]] +=== Data-Oriented Design + +Data-Oriented Design (DOD) focuses on organizing data for efficient processing, rather than organizing code around objects. + +image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Diagram, width=600] + +==== Key Concepts + +1. *Data Layout* - Organizing data for cache-friendly access patterns. +2. *Systems* - Process data in bulk, often using SIMD instructions. +3. *Entity-Component-System (ECS)* - A common implementation of DOD principles. + +==== Benefits of Data-Oriented Design + +* Better cache utilization +* More efficient memory usage +* Easier to parallelize +* Can lead to significant performance improvements + +==== Implementation Example + +[source,cpp] +---- +// A simple ECS implementation +struct TransformData { + std::vector positions; + std::vector rotations; + std::vector scales; +}; + +struct RenderData { + std::vector meshes; + std::vector materials; +}; + +class TransformSystem { +private: + TransformData& transformData; + +public: + TransformSystem(TransformData& data) : transformData(data) {} + + void Update(float deltaTime) { + // Process all transforms in bulk + for (size_t i = 0; i < transformData.positions.size(); ++i) { + // Update transforms + } + } +}; + +class RenderSystem { +private: + RenderData& renderData; + TransformData& transformData; + +public: + RenderSystem(RenderData& rData, TransformData& tData) + : renderData(rData), transformData(tData) {} + + void Render() { + // Render all entities in bulk + for (size_t i = 0; i < renderData.meshes.size(); ++i) { + // Render mesh with transform + } + } +}; +---- + +[[service-locator-pattern]] +=== Service Locator Pattern + +The Service Locator pattern provides a global point of access to services without coupling consumers to concrete implementations. + +image::../../../images/service_locator_pattern_diagram.svg[Service Locator Pattern Diagram, width=600] + +==== Key Concepts + +1. *Service Interface* - Defines the contract for a service. +2. *Service Provider* - Implements the service interface. +3. *Service Locator* - Provides access to services. + +==== Benefits of Service Locator Pattern + +* Decouples service consumers from service providers +* Allows for easy service replacement +* Facilitates testing with mock services + +==== Implementation Example + +[source,cpp] +---- +// Audio service interface +class IAudioService { +public: + virtual ~IAudioService() = default; + virtual void PlaySound(const std::string& soundName) = 0; + virtual void StopSound(const std::string& soundName) = 0; +}; + +// Concrete audio service +class OpenALAudioService : public IAudioService { +public: + void PlaySound(const std::string& soundName) override { + // Implementation using OpenAL + } + + void StopSound(const std::string& soundName) override { + // Implementation using OpenAL + } +}; + +// Service locator +class ServiceLocator { +private: + static IAudioService* audioService; + static IAudioService nullAudioService; // Default null service + +public: + static void Initialize() { + audioService = &nullAudioService; + } + + static IAudioService& GetAudioService() { + return *audioService; + } + + static void ProvideAudioService(IAudioService* service) { + if (service == nullptr) { + audioService = &nullAudioService; + } else { + audioService = service; + } + } +}; + +// Usage example +void PlayGameSound() { + ServiceLocator::GetAudioService().PlaySound("explosion"); +} +---- + +=== Comparative Analysis of Architectural Patterns + +Below is a comparative analysis of the architectural patterns discussed in this appendix: + +|=== +| Pattern | Strengths | Weaknesses | Best Used For + +| Layered Architecture +| * Clear separation of concerns + * Easy to understand + * Good for beginners +| * Can lead to "layer bloat" + * May introduce unnecessary indirection + * Potential performance overhead from layer traversal +| * Smaller engines + * Educational projects + * When clarity is more important than performance + +| Component-Based Architecture +| * Highly flexible and modular + * Promotes code reuse + * Avoids deep inheritance hierarchies + * Easier to extend with new features +| * More complex to implement initially + * Can be harder to debug + * Potential performance overhead from component lookups +| * Modern rendering engines + * Systems with diverse object types + * Projects requiring frequent extension + +| Data-Oriented Design +| * Excellent performance + * Cache-friendly memory access + * Good for parallel processing +| * Less intuitive than OOP + * Steeper learning curve + * Can make code harder to read +| * Performance-critical systems + * Mobile platforms + * Systems processing large amounts of similar data + +| Service Locator Pattern +| * Decouples service providers from consumers + * Facilitates testing + * Allows runtime service swapping +| * Can hide dependencies + * Potential for runtime errors + * Global state concerns +| * Cross-cutting concerns + * Systems requiring runtime configuration + * When loose coupling is critical +|=== + +== Advanced Rendering Techniques + +This section provides an overview of advanced rendering techniques commonly used in modern rendering engines. For more comprehensive information, refer to these excellent resources: + +* *Physically Based Rendering: From Theory to Implementation* - https://www.pbr-book.org/ +* *Real-Time Rendering* - https://www.realtimerendering.com/ +* *GPU Gems* series - https://developer.nvidia.com/gpugems/gpugems/contributors + +=== Deferred Rendering + +Deferred rendering separates the geometry and lighting calculations into separate passes, which can be more efficient for scenes with many lights: + +1. *Geometry Pass* - Render scene geometry to G-buffer textures (position, normal, albedo, etc.). +2. *Lighting Pass* - Apply lighting calculations using G-buffer textures. + +=== Forward+ Rendering + +Forward+ (or tiled forward) rendering combines the simplicity of forward rendering with some of the efficiency benefits of deferred rendering: + +1. *Light Culling Pass* - Divide the screen into tiles and determine which lights affect each tile. +2. *Forward Rendering Pass* - Render scene geometry with only the lights that affect each tile. + +=== Physically Based Rendering (PBR) + +PBR aims to create more realistic materials by simulating how light interacts with surfaces in the real world: + +1. *Material Parameters* - Define materials using physically meaningful parameters (albedo, metalness, roughness, etc.). +2. *BRDF* - Use a physically based bidirectional reflectance distribution function. +3. *Image-Based Lighting* - Use environment maps for ambient lighting. + +=== Advanced Camera Techniques + +This section covers advanced techniques for implementing sophisticated camera systems in 3D applications: + +* *Camera Collision*: Implement a collision volume for the camera to prevent it from passing through walls +* *Context-Aware Positioning*: Adjust camera position based on the environment (e.g., zoom out in large open areas, zoom in in tight spaces) +* *Intelligent Framing*: Adjust the camera to keep both the character and important objects in frame +* *Predictive Following*: Anticipate character movement to reduce camera lag +* *Camera Obstruction Transparency*: Make objects that obstruct the view partially transparent +* *Dynamic Field of View*: Adjust the FOV based on movement speed or environmental context + +== Conclusion + +These architectural patterns and rendering techniques provide a foundation for designing your rendering engine. In practice, most engines use a combination of these patterns to address different aspects of the system. + +When designing your engine architecture, consider: + +1. *Performance Requirements* - Different patterns have different performance characteristics. +2. *Flexibility Needs* - How much flexibility do you need for future extensions? +3. *Team Size and Experience* - More complex architectures may be harder to work with for smaller teams. +4. *Project Scope* - A small project may not need the complexity of a full ECS. + +xref:../Engine_Architecture/02_architectural_patterns.adoc[Back to Architectural Patterns] +xref:../Engine_Architecture/05_rendering_pipeline.adoc[Back to Rendering Pipeline] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc new file mode 100644 index 00000000..02ba0980 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/01_introduction.adoc @@ -0,0 +1,31 @@ +:pp: {plus}{plus} + += Camera & Transformations: Introduction +== Introduction + +Welcome to the "Camera & Transformations" chapter of our "Building a Simple Engine" series! In this chapter, we'll dive into the essential mathematics and techniques needed to implement a 3D camera system in Vulkan. + +Understanding how to manipulate 3D space is fundamental to creating interactive 3D applications. We'll explore the mathematical foundations of 3D transformations and implement a flexible camera system that will allow us to navigate and view our 3D scenes from any perspective. + +In this chapter, we'll focus on: + +* Understanding the mathematical foundations of 3D transformations +* Implementing different types of transformation matrices (model, view, projection) +* Creating a flexible camera system with different movement modes +* Handling user input to control the camera +* Integrating the camera system with our Vulkan rendering pipeline + +By the end of this chapter, you'll have a solid understanding of 3D transformations and a reusable camera system that can be integrated into your Vulkan applications. + +== Prerequisites + +Before starting this chapter, you should have completed the main Vulkan tutorial. You should also be familiar with: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* Basic programming concepts and C++ + +xref:02_math_foundations.adoc[Next: Mathematical Foundations] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc new file mode 100644 index 00000000..f217cb6e --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc @@ -0,0 +1,1105 @@ +:pp: {plus}{plus} + += Camera & Transformations: Mathematical Foundations + +== Mathematical Foundations for 3D Graphics + +Before diving into camera implementation, let's review the essential mathematical concepts that form the foundation of 3D graphics programming. Understanding these concepts is crucial for implementing a robust camera system. + +=== Vectors in 3D Graphics + +Vectors are fundamental to 3D graphics as they represent positions, directions, and movements in space. In our Vulkan application, we'll primarily work with: + +* *3D vectors (x, y, z)*: Used for positions, directions, and normals +* *4D vectors (x, y, z, w)*: Used for homogeneous coordinates in transformations + +==== Why Vectors Matter in Graphics + +In our camera system, vectors serve several critical purposes: + +* The camera's position is represented as a 3D vector +* The camera's viewing direction is a 3D vector +* The "up" direction that orients the camera is also a vector + +==== Vector Operations and Their Applications + +* *Addition and Subtraction*: Used for calculating relative positions and movements + - Example: `newPosition = currentPosition + movementDirection * speed` + +* *Scalar Multiplication*: Used for scaling movements and directions + - Example: Slowing down camera movement by multiplying velocity by a factor < 1 + +* *Dot Product*: Calculates the cosine of the angle between vectors (when normalized) + - Applications: Determining if objects are facing the camera, calculating lighting intensity + +==== The Right-Hand Rule + +The right-hand rule is a convention used in 3D graphics and mathematics to determine the orientation of coordinate systems and the direction of cross-products. + +* *For Cross Products*: When calculating A × B: + + 1. Point your right hand's index finger in the direction of vector A + 2. Point your middle finger in the direction of vector B (perpendicular to A) + 3. Your thumb now points in the direction of the resulting cross-product + +* *For Coordinate Systems*: In a right-handed coordinate system: + + 1. Point your right hand's index finger along the positive X-axis + 2. Point your middle finger along the positive Y-axis + 3. Your thumb points along the positive Z-axis + +[source,cpp] +---- +// The cross product direction follows the right-hand rule +glm::vec3 xAxis(1.0f, 0.0f, 0.0f); // Point right (positive X) +glm::vec3 yAxis(0.0f, 1.0f, 0.0f); // Point up (positive Y) + +// Cross product gives the Z axis in a right-handed system +glm::vec3 zAxis = glm::cross(xAxis, yAxis); // Points forward (positive Z) +// zAxis will be (0.0f, 0.0f, 1.0f) + +// If we reverse the order, we get the opposite direction +glm::vec3 negativeZ = glm::cross(yAxis, xAxis); // Points backward (negative Z) +// negativeZ will be (0.0f, 0.0f, -1.0f) +---- + +* *Cross Product*: Creates a vector perpendicular to two input vectors + - Applications: Generating the camera's "right" vector from "forward" and "up" vectors + - The direction follows the right-hand rule (explained above) + +* *Normalization*: Preserves the direction while setting length to 1 + - Applications: Ensuring consistent movement speed regardless of direction + +[source,cpp] +---- +// Vector operations using GLM +glm::vec3 a(1.0f, 2.0f, 3.0f); +glm::vec3 b(4.0f, 5.0f, 6.0f); + +// Addition - combining positions or offsets +glm::vec3 sum = a + b; // (5.0, 7.0, 9.0) + +// Dot product - useful for lighting calculations +float dotProduct = glm::dot(a, b); // 32.0 +// If vectors are normalized, dot product = cosine of angle between them +float cosAngle = glm::dot(glm::normalize(a), glm::normalize(b)); // ~0.974 + +// Cross product - creating perpendicular vectors (e.g., camera orientation) +glm::vec3 crossProduct = glm::cross(a, b); // (-3.0, 6.0, -3.0) + +// Normalization - ensuring consistent movement speeds +glm::vec3 normalized = glm::normalize(a); // (0.267, 0.535, 0.802) +---- + + +=== Matrices and Transformations + +Matrices are used to represent transformations in 3D space. In Vulkan and other graphics APIs, we typically use 4×4 matrices to represent transformations in homogeneous coordinates. + +==== Why We Use 4×4 Matrices + +Even though we work in 3D space, we use 4×4 matrices because: + +1. They allow us to represent translation (movement) along with rotation and scaling +2. They can be combined (multiplied) to create complex transformations +3. They work with homogeneous coordinates (x, y, z, w) which are required for perspective projection + +==== Common Transformation Matrices + +* *Translation Matrix*: Moves objects in 3D space + - In a camera system: Moving the camera position + +* *Rotation Matrix*: Rotates objects around an axis + - In a camera system: Changing where the camera is looking + +* *Scale Matrix*: Changes the size of objects + - Less commonly used for cameras, but important for objects in the scene + +* *Model Matrix*: Combines transformations to position an object in world space + - Positions the objects relative to the world origin + +* *View Matrix*: Transforms world space to camera space + - Essentially positions the world relative to the camera + +* *Projection Matrix*: Transforms camera space to clip space + - Defines how 3D objects are projected onto the 2D screen + - Controls perspective, field of view, and visible range (near/far planes) + +[source,cpp] +---- +// Matrix transformations using GLM +// Translation matrix - moving an object +glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), glm::vec3(1.0f, 2.0f, 3.0f)); + +// Rotation matrix (45 degrees around Y axis) - turning an object +glm::mat4 rotationMatrix = glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f)); + +// Scale matrix - resizing an object +glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), glm::vec3(2.0f, 2.0f, 2.0f)); + +// Combining transformations (scale, then rotate, then translate) +// Order matters! The rightmost transformation is applied first +glm::mat4 modelMatrix = translationMatrix * rotationMatrix * scaleMatrix; +---- + +==== Matrix Order Matters + +The order of matrix multiplication is crucial because transformations are applied from right to left. Getting the order wrong can completely change your object's final position and orientation. + +Consider this practical example: if you want to rotate a cube around its own center and then move it to a new position, you must apply the transformations in the correct order: + +[source,cpp] +---- +// CORRECT: Scale first, then rotate, then translate +// This rotates the cube around its own center, then moves it +glm::mat4 modelMatrix = translationMatrix * rotationMatrix * scaleMatrix; + +// WRONG: Translate first, then rotate +// This would move the cube away from origin, then rotate it around the world origin +// The cube would orbit around the world center instead of rotating in place! +glm::mat4 wrongMatrix = rotationMatrix * translationMatrix * scaleMatrix; +---- + +For our camera pipeline: `projectionMatrix * viewMatrix * modelMatrix * vertex` +Each transformation prepares the data for the next stage, and changing this order would break the rendering pipeline. + +==== Visual Example: Why Matrix Order Matters + +The following diagram illustrates the difference between correct and incorrect matrix multiplication order when transforming a cube: + +.Matrix Transformation Order Comparison +image::../../../images/matrix-order-comparison.svg[Matrix Order Comparison showing correct T×R×S vs incorrect R×T×S transformation sequences] + +==== Row-Major vs. Column-Major Representation + +When working with matrices in graphics programming, it's important to understand the difference between row-major and column-major representations: + +* *Row-Major*: Matrix elements are stored row by row in memory + - Used by DirectX, C/C++ multi-dimensional arrays + - A matrix is accessed as `M[row][column]` + +* *Column-Major*: Matrix elements are stored column by column in memory + - Used by OpenGL, GLSL, and by default in GLM + - A matrix is accessed as `M[column][row]` (in memory layout terms) + +[source,cpp] +---- +// Row-major vs Column-major representation of a 3x3 matrix +// For a matrix: +// [ a b c ] +// [ d e f ] +// [ g h i ] + +// Row-major memory layout: +// [a, b, c, d, e, f, g, h, i] + +// Column-major memory layout: +// [a, d, g, b, e, h, c, f, i] + +// In GLM, matrices are column-major by default +glm::mat4 matrix = glm::mat4(1.0f); // Identity matrix in column-major format + +// When passing matrices to Vulkan shaders, you need to be aware of the layout +// Vulkan expects column-major by default, matching GLM's default +---- + +==== Vulkan and Matrix Layouts + +Vulkan works with both row-major and column-major formats, but you need to specify which one you're using: + +* By default, Vulkan expects matrices in column-major format +* You can specify row-major format in your shaders using the `row_major` qualifier +* GLM (commonly used with Vulkan) uses column-major by default, but can be configured for row-major + +The practical implications: + +* Matrix multiplication order may need to be reversed depending on the layout +* When debugging, matrix elements may appear transposed compared to mathematical notation +* When porting code between different APIs, matrix layouts may need to be transposed + +=== Affine Transformations + +Affine transformations are a fundamental concept in computer graphics that preserve parallel lines (but not necessarily angles or distances). They're essential for representing most common operations in 3D graphics. + +==== Properties of Affine Transformations + +An affine transformation can be represented as a combination of: + +* Linear transformations (rotation, scaling, shearing) +* Translation (movement) + +In mathematical terms, an affine transformation can be expressed as: + +[stem] +++++ +f(x) = Ax + b +++++ + +where A is a matrix (linear transformation) and b is a vector (translation). + +==== Why Affine Transformations Matter in Graphics + +* They preserve collinearity (points on a line remain on a line) +* They preserve ratios of distances along a line +* They can represent all the common transformations we need in 3D graphics +* They can be efficiently composed (combined) through matrix multiplication + +==== Representing Affine Transformations with Homogeneous Coordinates + +In 3D graphics, we use 4×4 matrices to represent affine transformations using homogeneous coordinates: + +[source,cpp] +---- +// A 4×4 matrix representing an affine transformation +// [ R R R Tx ] +// [ R R R Ty ] +// [ R R R Tz ] +// [ 0 0 0 1 ] +// Where R represents rotation/scaling/shearing and T represents translation + +// Example of an affine transformation matrix in GLM +glm::mat4 affineTransform = glm::mat4( + glm::vec4(r11, r12, r13, tx), // First row + glm::vec4(r21, r22, r23, ty), // Second row + glm::vec4(r31, r32, r33, tz), // Third row + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) // Last row is always (0,0,0,1) for affine transformations +); +---- + +==== Affine Transformations in Practice + +In our Vulkan application, almost all transformations we perform are affine: +* Moving objects around the scene (translation) +* Rotating objects to face different directions +* Scaling objects to make them larger or smaller +* Combining these operations to position and orient objects + +=== Pose Matrices + +A pose matrix (also called a transformation matrix or rigid body transformation) is a specific type of affine transformation that represents both the position and orientation of an object in 3D space. + +==== Structure of a Pose Matrix + +A pose matrix combines rotation and translation in a single 4×4 matrix: + +[source,cpp] +---- +// A pose matrix has this structure: +// [ R R R Tx ] +// [ R R R Ty ] +// [ R R R Tz ] +// [ 0 0 0 1 ] +// Where the 3×3 R submatrix represents rotation and [Tx,Ty,Tz] represents translation + +// Creating a pose matrix in GLM +glm::mat4 poseMatrix = glm::mat4(1.0f); // Start with identity matrix +poseMatrix = glm::translate(poseMatrix, position); // Apply translation +poseMatrix = poseMatrix * rotationMatrix; // Apply rotation +---- + +==== Applications of Pose Matrices + +Pose matrices are essential in graphics engines for: + +* *Object Positioning*: Defining where objects are located and how they're oriented + - Example: Placing a character model in the world with the correct position and facing direction + +* *Camera Representation*: Defining the camera's position and orientation + - Example: The view matrix is the inverse of the camera's pose matrix + +* *Hierarchical Transformations*: Building complex objects from simpler parts + - Example: A character's hand position depends on the arm position, which depends on the torso position + +* *Animation*: Interpolating between different poses + - Example: Smoothly transitioning a camera from one position/orientation to another + +==== Extracting Information from Pose Matrices + +We can extract useful information from pose matrices: + +[source,cpp] +---- +// Extracting position from a pose matrix +glm::vec3 extractPosition(const glm::mat4& poseMatrix) { + return glm::vec3(poseMatrix[3]); // The translation is stored in the last column +} + +// Extracting forward direction (assuming standard OpenGL orientation) +glm::vec3 extractForwardDirection(const glm::mat4& poseMatrix) { + return -glm::vec3(poseMatrix[2]); // Negative Z axis (third column) +} + +// Extracting up direction +glm::vec3 extractUpDirection(const glm::mat4& poseMatrix) { + return glm::vec3(poseMatrix[1]); // Y axis (second column) +} +---- + +=== Implementing a Look-At Function + +A "look-at" function is a fundamental tool in camera systems that creates a view matrix to orient the camera towards a specific target point. This is one of the most common operations in 3D graphics and provides an excellent example of how the mathematical concepts we've discussed are applied in practice. + +==== Purpose of the Look-At Function + +The look-at function serves several important purposes: + +* Orients the camera to face a specific point in 3D space +* Establishes the camera's local coordinate system (right, up, forward vectors) +* Creates a view matrix that transforms world coordinates into camera space +* Simplifies camera control by focusing on a target rather than managing rotation angles + +==== Mathematical Principles + +The look-at function works by constructing an orthonormal basis (three perpendicular unit vectors) that defines the camera's orientation: + +1. *Forward Vector (Z)*: Points from the camera position to the target position +2. *Right Vector (X)*: Perpendicular to both the forward vector and the world up vector +3. *Up Vector (Y)*: Perpendicular to both the forward and right vectors + +These three vectors, along with the camera position, form the view matrix that transforms world coordinates into camera space. + +==== Step-by-Step Implementation + +Let's implement a custom look-at function to understand how it works: + +[source,cpp] +---- +glm::mat4 createLookAtMatrix( + const glm::vec3& cameraPosition, // Where the camera is + const glm::vec3& targetPosition, // What the camera is looking at + const glm::vec3& worldUpVector // Which way is "up" (usually Y axis) +) { + // Step 1: Calculate the camera's forward direction (Z axis) + // Note: We negate this because in OpenGL/Vulkan, the camera looks down the negative Z-axis + glm::vec3 forward = glm::normalize(cameraPosition - targetPosition); + + // Step 2: Calculate the camera's right direction (X axis) + // Using cross product between world up and forward direction + glm::vec3 right = glm::normalize(glm::cross(worldUpVector, forward)); + + // Step 3: Calculate the camera's up direction (Y axis) + // Using cross product between forward and right to ensure orthogonality + glm::vec3 up = glm::cross(forward, right); + + // Step 4: Construct the rotation part of the view matrix + // Each row contains one of the camera's basis vectors + glm::mat4 rotation = glm::mat4(1.0f); + rotation[0][0] = right.x; + rotation[1][0] = right.y; + rotation[2][0] = right.z; + rotation[0][1] = up.x; + rotation[1][1] = up.y; + rotation[2][1] = up.z; + rotation[0][2] = forward.x; + rotation[1][2] = forward.y; + rotation[2][2] = forward.z; + + // Step 5: Construct the translation part of the view matrix + glm::mat4 translation = glm::mat4(1.0f); + translation[3][0] = -cameraPosition.x; + translation[3][1] = -cameraPosition.y; + translation[3][2] = -cameraPosition.z; + + // Step 6: Combine rotation and translation + // The translation is applied first, then the rotation + return rotation * translation; +} +---- + +==== Using GLM's Built-in Look-At Function + +In practice, we typically use GLM's built-in `lookAt` function, which implements the same algorithm: + +[source,cpp] +---- +// Using GLM's built-in lookAt function +glm::mat4 viewMatrix = glm::lookAt( + glm::vec3(0.0f, 0.0f, 5.0f), // Camera position + glm::vec3(0.0f, 0.0f, 0.0f), // Target position (origin) + glm::vec3(0.0f, 1.0f, 0.0f) // World up vector (Y axis) +); +---- + +==== Practical Applications + +The look-at function is used in various scenarios: + +* *First-Person Camera*: Looking in the direction of movement +* *Third-Person Camera*: Following a character while looking at them +* *Orbit Camera*: Circling around a point of interest +* *Cinematic Camera*: Creating smooth camera movements that focus on important objects +* *Object Inspection*: Allowing users to examine 3D models from different angles + +==== Example: Implementing an Orbit Camera + +Here's how you might use the look-at function to implement an orbit camera that circles around a target: + +[source,cpp] +---- +// Orbit camera implementation +void updateOrbitCamera(float deltaTime) { + // Update the orbit angle based on time + orbitAngle += orbitSpeed * deltaTime; + + // Calculate the camera position on a circle around the target + float radius = 10.0f; + glm::vec3 cameraPosition( + targetPosition.x + radius * cos(orbitAngle), + targetPosition.y + 5.0f, // Slightly above the target + targetPosition.z + radius * sin(orbitAngle) + ); + + // Create the view matrix using lookAt + viewMatrix = glm::lookAt( + cameraPosition, + targetPosition, + glm::vec3(0.0f, 1.0f, 0.0f) + ); +} +---- + +==== Example: Smooth Camera Transitions + +The look-at function can also be used to create smooth transitions between different camera positions and targets: + +[source,cpp] +---- +// Smooth camera transition +void transitionCamera(float t) { // t ranges from 0.0 to 1.0 + // Interpolate between start and end positions + glm::vec3 currentPosition = glm::mix(startPosition, endPosition, t); + + // Interpolate between start and end targets + glm::vec3 currentTarget = glm::mix(startTarget, endTarget, t); + + // Update the view matrix + viewMatrix = glm::lookAt( + currentPosition, + currentTarget, + glm::vec3(0.0f, 1.0f, 0.0f) + ); +} +---- + +By understanding how the look-at function works, you gain insight into how cameras are oriented in 3D space and how the view matrix transforms the world from the camera's perspective. + +=== Raycasting in 3D Graphics + +Raycasting is a fundamental technique in 3D graphics that involves projecting rays from a point into the scene and determining what they intersect with. It's used for a wide range of applications, from picking objects in a scene to implementing collision detection and visibility determination. + +==== Ray Representation + +A ray in 3D space is defined by an origin point and a direction vector: + +[source,cpp] +---- +struct Ray { + glm::vec3 origin; // Starting point of the ray + glm::vec3 direction; // Normalized direction vector +}; + +// Creating a ray +Ray createRay(const glm::vec3& origin, const glm::vec3& direction) { + Ray ray; + ray.origin = origin; + ray.direction = glm::normalize(direction); // Ensure direction is normalized + return ray; +} +---- + +==== Ray-Object Intersection + +The core of raycasting is determining if and where a ray intersects with objects in the scene. Let's look at some common intersection tests: + +===== Ray-Sphere Intersection + +One of the simplest intersection tests is between a ray and a sphere: + +[source,cpp] +---- +struct Sphere { + glm::vec3 center; + float radius; +}; + +bool rayIntersectsSphere(const Ray& ray, const Sphere& sphere, float& t) { + // Vector from ray origin to sphere center + glm::vec3 oc = ray.origin - sphere.center; + + // Quadratic equation coefficients + float a = glm::dot(ray.direction, ray.direction); // Always 1 if direction is normalized + float b = 2.0f * glm::dot(oc, ray.direction); + float c = glm::dot(oc, oc) - sphere.radius * sphere.radius; + + // Discriminant + float discriminant = b * b - 4 * a * c; + + if (discriminant < 0) { + // No intersection + return false; + } + + // Find the nearest intersection point + float sqrtDiscriminant = sqrt(discriminant); + float t0 = (-b - sqrtDiscriminant) / (2 * a); + float t1 = (-b + sqrtDiscriminant) / (2 * a); + + // Check if intersection is in front of the ray + if (t0 > 0) { + t = t0; + return true; + } + + if (t1 > 0) { + t = t1; + return true; + } + + // Both intersections are behind the ray + return false; +} +---- + +===== Ray-Triangle Intersection + +Triangle intersection is essential for raycasting against 3D models: + +[source,cpp] +---- +struct Triangle { + glm::vec3 v0, v1, v2; // Vertices +}; + +bool rayIntersectsTriangle(const Ray& ray, const Triangle& triangle, float& t, glm::vec2& barycentricCoords) { + // Möller–Trumbore algorithm + glm::vec3 edge1 = triangle.v1 - triangle.v0; + glm::vec3 edge2 = triangle.v2 - triangle.v0; + glm::vec3 h = glm::cross(ray.direction, edge2); + float a = glm::dot(edge1, h); + + // Check if ray is parallel to triangle + if (a > -0.00001f && a < 0.00001f) { + return false; + } + + float f = 1.0f / a; + glm::vec3 s = ray.origin - triangle.v0; + float u = f * glm::dot(s, h); + + // Check if intersection is outside triangle + if (u < 0.0f || u > 1.0f) { + return false; + } + + glm::vec3 q = glm::cross(s, edge1); + float v = f * glm::dot(ray.direction, q); + + // Check if intersection is outside triangle + if (v < 0.0f || u + v > 1.0f) { + return false; + } + + // Compute intersection distance + t = f * glm::dot(edge2, q); + + // Check if intersection is behind the ray + if (t <= 0.0f) { + return false; + } + + // Store barycentric coordinates for interpolation + barycentricCoords = glm::vec2(u, v); + return true; +} +---- + +===== Ray-AABB Intersection + +Axis-Aligned Bounding Box (AABB) intersection is useful for broad-phase collision detection: + +[source,cpp] +---- +struct AABB { + glm::vec3 min; // Minimum corner + glm::vec3 max; // Maximum corner +}; + +bool rayIntersectsAABB(const Ray& ray, const AABB& aabb, float& tMin, float& tMax) { + // Compute intersection with each slab + glm::vec3 invDir = 1.0f / ray.direction; + glm::vec3 t0 = (aabb.min - ray.origin) * invDir; + glm::vec3 t1 = (aabb.max - ray.origin) * invDir; + + // Handle negative directions + glm::vec3 tSmaller = glm::min(t0, t1); + glm::vec3 tBigger = glm::max(t0, t1); + + // Find entry and exit points + tMin = glm::max(tSmaller.x, glm::max(tSmaller.y, tSmaller.z)); + tMax = glm::min(tBigger.x, glm::min(tBigger.y, tBigger.z)); + + // Check if there's a valid intersection + return tMax >= tMin && tMax > 0; +} +---- + +==== Creating Camera Rays + +One of the most common uses of raycasting is to create rays from the camera into the scene, which is essential for picking objects or implementing ray tracing: + +[source,cpp] +---- +Ray createCameraRay( + const glm::vec2& screenCoord, // Normalized screen coordinates (-1 to 1) + const glm::mat4& viewMatrix, // Camera view matrix + const glm::mat4& projectionMatrix // Camera projection matrix +) { + // Convert to clip space + glm::vec4 clipCoords(screenCoord.x, screenCoord.y, -1.0f, 1.0f); + + // Convert to view space + glm::mat4 invProjection = glm::inverse(projectionMatrix); + glm::vec4 viewCoords = invProjection * clipCoords; + viewCoords.z = -1.0f; // Point towards negative Z in view space + viewCoords.w = 0.0f; // Convert to direction vector + + // Convert to world space + glm::mat4 invView = glm::inverse(viewMatrix); + glm::vec4 worldCoords = invView * viewCoords; + + // Create ray + Ray ray; + ray.origin = glm::vec3(invView[3]); // Camera position in world space + ray.direction = glm::normalize(glm::vec3(worldCoords)); + + return ray; +} +---- + +==== Applications of Raycasting in Graphics + +Raycasting has numerous applications in 3D graphics and game development: + +* *Object Picking*: Determining which object the user clicked on in a 3D scene + - Cast a ray from the camera through the mouse position and find the nearest intersection + +* *Collision Detection*: Checking if objects will collide along a movement path + - Cast rays in the direction of movement to detect potential collisions + +* *Line of Sight*: Determining if one object can "see" another + - Cast a ray between two objects and check for obstructions + +* *Terrain Height Sampling*: Finding the height of terrain at a specific point + - Cast a ray downward from above the terrain + +* *Physics Simulations*: Implementing realistic physics behaviors + - Raycasting is fundamental to many physics engines for collision resolution + +* *AI Navigation*: Helping AI characters navigate environments + - Raycasting can detect obstacles and determine valid paths + +==== Optimizing Raycasting Performance + +For complex scenes with many objects, raycasting can become computationally expensive. Here are some optimization techniques: + +* *Spatial Partitioning*: Use data structures like octrees, BVHs, or k-d trees to quickly eliminate objects that can't possibly intersect with the ray + +* *Bounding Volume Hierarchies*: Test against simple bounding volumes (spheres, AABBs) before performing more expensive tests against detailed geometry + +* *Level of Detail*: Use simpler collision geometry for distant objects + +* *Ray Batching*: Process multiple rays together to take advantage of SIMD instructions + +* *Early Termination*: Stop testing once you've found the closest intersection (if that's all you need) + +=== Projection in 3D Graphics + +Projection is the process of transforming 3D coordinates in view space to 2D coordinates on the screen. In computer graphics, we use projection matrices to perform this transformation. + +==== Types of Projection + +There are two main types of projection used in 3D graphics: + +* *Perspective Projection*: Objects appear smaller as they get farther away, simulating how we see the world +* *Orthographic Projection*: Objects maintain their size regardless of distance, useful for technical drawings, 2D games, and UI elements + +==== Perspective Projection + +Perspective projection creates a realistic view where distant objects appear smaller, creating the illusion of depth: + +[source,cpp] +---- +// Creating a perspective projection matrix +glm::mat4 createPerspectiveMatrix( + float fovY, // Vertical field of view in degrees + float aspectRatio, // Width / height of the viewport + float nearPlane, // Distance to the near clipping plane + float farPlane // Distance to the far clipping plane +) { + return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane); +} +---- + +The perspective projection matrix performs several transformations: + +1. Scales the view frustum based on the field of view and aspect ratio +2. Maps the view volume to a canonical view volume (a cube from -1 to 1 in each dimension) +3. Applies perspective division (dividing by w) to create the perspective effect + +The resulting matrix has this structure: + +[source,cpp] +---- +// Structure of a perspective projection matrix +// [ (h/w)*cot(fovY/2) 0 0 0 ] +// [ 0 cot(fovY/2) 0 0 ] +// [ 0 0 -(f+n)/(f-n) -2*f*n/(f-n) ] +// [ 0 0 -1 0 ] +// Where: +// - fovY is the vertical field of view +// - w/h is the aspect ratio +// - n is the near plane distance +// - f is the far plane distance +---- + +==== Orthographic Projection + +Orthographic projection maintains the size of objects regardless of their distance from the camera: + +[source,cpp] +---- +// Creating an orthographic projection matrix +glm::mat4 createOrthographicMatrix( + float left, // Left plane coordinate + float right, // Right plane coordinate + float bottom, // Bottom plane coordinate + float top, // Top plane coordinate + float nearPlane, // Near plane distance + float farPlane // Far plane distance +) { + return glm::ortho(left, right, bottom, top, nearPlane, farPlane); +} +---- + +The orthographic projection matrix simply scales and translates the view volume to the canonical view volume without applying any perspective division: + +[source,cpp] +---- +// Structure of an orthographic projection matrix +// [ 2/(r-l) 0 0 -(r+l)/(r-l) ] +// [ 0 2/(t-b) 0 -(t+b)/(t-b) ] +// [ 0 0 -2/(f-n) -(f+n)/(f-n) ] +// [ 0 0 0 1 ] +// Where: +// - l, r are the left and right planes +// - b, t are the bottom and top planes +// - n, f are the near and far planes +---- + +==== The View Frustum + +The view frustum is the volume of space visible to the camera. For perspective projection, it's a truncated pyramid: + +* *Near Plane*: The closest plane to the camera where rendering begins +* *Far Plane*: The farthest plane from the camera where rendering ends +* *Field of View (FOV)*: The angle that determines how wide the view is +* *Aspect Ratio*: The ratio of width to height of the viewport + +[source,cpp] +---- +// Calculating the corners of the view frustum +void calculateFrustumCorners( + float fovY, + float aspectRatio, + float nearPlane, + float farPlane, + glm::vec3 corners[8] // Output array for the 8 corners +) { + float tanHalfFovY = tan(glm::radians(fovY) / 2.0f); + + // Near plane dimensions + float nearHeight = 2.0f * nearPlane * tanHalfFovY; + float nearWidth = nearHeight * aspectRatio; + + // Far plane dimensions + float farHeight = 2.0f * farPlane * tanHalfFovY; + float farWidth = farHeight * aspectRatio; + + // Near plane corners (in view space) + corners[0] = glm::vec3(-nearWidth/2, -nearHeight/2, -nearPlane); // Bottom-left + corners[1] = glm::vec3( nearWidth/2, -nearHeight/2, -nearPlane); // Bottom-right + corners[2] = glm::vec3( nearWidth/2, nearHeight/2, -nearPlane); // Top-right + corners[3] = glm::vec3(-nearWidth/2, nearHeight/2, -nearPlane); // Top-left + + // Far plane corners (in view space) + corners[4] = glm::vec3(-farWidth/2, -farHeight/2, -farPlane); // Bottom-left + corners[5] = glm::vec3( farWidth/2, -farHeight/2, -farPlane); // Bottom-right + corners[6] = glm::vec3( farWidth/2, farHeight/2, -farPlane); // Top-right + corners[7] = glm::vec3(-farWidth/2, farHeight/2, -farPlane); // Top-left +} +---- + +==== Projection and Unprojection + +Projection converts 3D world coordinates to 2D screen coordinates, while unprojection does the reverse. The following code examples demonstrate these concepts for educational purposes: + +[NOTE] +==== +These utility functions are provided to help understand the mathematical concepts behind projection and unprojection. While they may not be directly used in the basic rendering pipeline, they are valuable for implementing features like object picking, mouse interaction with 3D objects, and custom rendering techniques. +==== + +[source,cpp] +---- +// Project a 3D point to screen space +glm::vec2 projectPoint( + const glm::vec3& worldPoint, + const glm::mat4& viewMatrix, + const glm::mat4& projectionMatrix, + const glm::vec4& viewport // (x, y, width, height) +) { + // Transform to clip space + glm::vec4 clipSpace = projectionMatrix * viewMatrix * glm::vec4(worldPoint, 1.0f); + + // Perspective division + glm::vec3 ndcSpace = glm::vec3(clipSpace) / clipSpace.w; + + // Map to viewport + glm::vec2 screenPos; + screenPos.x = (ndcSpace.x + 1.0f) * 0.5f * viewport.z + viewport.x; + screenPos.y = (1.0f - ndcSpace.y) * 0.5f * viewport.w + viewport.y; // Y is flipped + + return screenPos; +} + +// Unproject a screen point to a ray in world space +Ray unprojectScreenPoint( + const glm::vec2& screenPoint, + const glm::mat4& viewMatrix, + const glm::mat4& projectionMatrix, + const glm::vec4& viewport // (x, y, width, height) +) { + // Convert to normalized device coordinates + glm::vec3 ndcPos; + ndcPos.x = 2.0f * (screenPoint.x - viewport.x) / viewport.z - 1.0f; + ndcPos.y = 1.0f - 2.0f * (screenPoint.y - viewport.y) / viewport.w; // Y is flipped + ndcPos.z = -1.0f; // Near plane + + // Create ray from camera through this point + return createCameraRay(glm::vec2(ndcPos.x, ndcPos.y), viewMatrix, projectionMatrix); +} +---- + +==== Applications of Projection in Graphics + +Projection matrices are used in various ways in 3D graphics: + +* *Rendering*: Converting 3D scene geometry to 2D screen pixels +* *Shadow Mapping*: Projecting the scene from a light's perspective to determine shadows +* *Reflection/Refraction*: Calculating how light bounces off or passes through surfaces +* *Texture Projection*: Mapping textures onto surfaces based on a projector's perspective +* *Screen-Space Effects*: Implementing post-processing effects like screen-space reflections or ambient occlusion + +==== Choosing the Right Projection + +The choice between perspective and orthographic projection depends on the application: + +* *Use Perspective Projection for*: + - First-person or third-person games + - Realistic 3D visualizations + - Any application where depth perception is important + +* *Use Orthographic Projection for*: + - 2D games with 3D elements + - Technical drawings and CAD applications + - UI elements that shouldn't be affected by perspective + - Isometric or top-down games + +=== Quaternions for Rotations + +While rotation matrices work well, quaternions offer advantages for certain rotation operations, particularly for smooth camera movements and avoiding "gimbal lock" (loss of a degree of freedom in certain orientations). + +==== Why Use Quaternions? + +* More compact representation (4 components vs. 9 for a rotation matrix) +* Easier to interpolate smoothly between orientations (important for camera animations) +* Avoids gimbal lock issues that can occur with Euler angles (pitch, yaw, roll) + +[source,cpp] +---- +// Quaternion operations using GLM +// Create a quaternion from Euler angles (in radians) +glm::quat rotation = glm::quat(glm::vec3( + glm::radians(30.0f), // pitch (X) - looking up/down + glm::radians(45.0f), // yaw (Y) - looking left/right + glm::radians(60.0f) // roll (Z) - tilting the camera +)); + +// Convert quaternion to rotation matrix for use in rendering +glm::mat4 rotationMatrix = glm::mat4_cast(rotation); + +// Rotate a vector using a quaternion (e.g., rotating the camera's forward vector) +glm::vec3 original(1.0f, 0.0f, 0.0f); +glm::vec3 rotated = rotation * original; +---- + +=== Coordinate Systems in 3D Graphics + +Understanding the different coordinate systems is essential for implementing a camera system. As data moves through the rendering pipeline, it undergoes several transformations: + +* *Local Space (Object Space)*: Coordinates relative to the object's origin + - Where vertices are initially defined relative to their own object + +* *World Space*: Coordinates relative to the world origin + - Where objects are positioned relative to each other in the scene + +* *View Space (Camera Space)*: Coordinates relative to the camera + - The world as seen from the camera's position and orientation + - The camera is at the origin (0,0,0) looking down the negative Z-axis + +* *Clip Space*: Coordinates after projection, in the range [-w, w] for each axis + - Determines what's visible on screen (inside the view frustum) + +* *Screen Space*: Final 2D coordinates for display on the screen + - The actual pixel positions where objects appear + +==== Handedness of Coordinate Systems + +Graphics APIs and engines use either right-handed or left-handed coordinate systems: + +* *Right-Handed System* (used by OpenGL and Vulkan by convention): + - X-axis points right + - Y-axis points up + - Z-axis points out of the screen (toward the viewer) + - Cross product: Z = X × Y (using the right-hand rule) + +* *Left-Handed System* (used by DirectX): + - X-axis points right + - Y-axis points up + - Z-axis points into the screen (away from the viewer) + - Cross product: Z = X × Y (using the left-hand rule) + +[source,cpp] +---- +// In Vulkan, we typically use a right-handed coordinate system +// But we can convert between systems if needed + +// Converting a point from left-handed to right-handed system +// (just flip the Z coordinate) +glm::vec3 leftHandedPoint(x, y, z); +glm::vec3 rightHandedPoint(x, y, -z); + +// When setting up a camera, the handedness affects the view matrix +// In a right-handed system, the camera typically looks down the negative Z-axis +// This is why we often see -Z as the "forward" direction in camera code +---- + +==== Implications for Camera Systems + +The handedness of your coordinate system affects how you set up your camera: + +* In a right-handed system (Vulkan convention): + - The camera typically looks down the negative Z-axis + - The "look" vector is often stored as a negative Z direction + - The view matrix is constructed using the right-hand rule for cross products + +* When extracting axes from a view matrix: + - Right vector: X-axis of the view matrix + - Up vector: Y-axis of the view matrix + - Forward vector: Negative Z-axis of the view matrix + +==== The Transformation Pipeline + +The transformation pipeline typically follows this sequence: +Local Space → World Space → View Space → Clip Space → Screen Space + +[source,cpp] +---- +// A typical vertex transformation in a shader +gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0); +---- + +In the next section, we'll implement these mathematical concepts to create a flexible camera system for our Vulkan application. + +=== Further Resources + +If you're finding some of the mathematical concepts challenging or want to deepen your understanding, here are some helpful resources organized by topic: + +==== General 3D Math Resources + +* *Books*: + - "Mathematics for 3D Game Programming and Computer Graphics" by Eric Lengyel - Comprehensive reference for 3D math + - "3D Math Primer for Graphics and Game Development" by Fletcher Dunn and Ian Parberry - Excellent beginner-friendly introduction + - "Essential Mathematics for Games and Interactive Applications" by James M. Van Verth and Lars M. Bishop - Practical approach with code examples + +* *Online Courses*: + - https://www.khanacademy.org/math/linear-algebra[Khan Academy Linear Algebra] - Free course covering vector and matrix fundamentals + - https://www.coursera.org/learn/linear-algebra-machine-learning[Mathematics for Machine Learning: Linear Algebra] - Covers vectors, matrices, and transformations + +* *Interactive Tools*: + - https://eater.net/quaternions[Quaternion Visualizer] - Interactive visualization of quaternion rotations + - https://math.hws.edu/graphicsbook/c3/s5.html[Interactive 3D Transformations] - Experiment with different transformations + +==== Vectors and Vector Operations + +* *Tutorials*: + - https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/vectors.html[Scratchapixel: Vectors] - Detailed explanation with graphics + - https://www.youtube.com/watch?v=fNk_zzaMoSs&list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab[3Blue1Brown: Essence of Linear Algebra] - Excellent visual explanations of vectors + +* *Interactive Tools*: + - https://www.geogebra.org/m/qCHzkpXh[GeoGebra: Vector Operations] - Interactive vector addition, subtraction, dot and cross products + - https://www.falstad.com/dotproduct/[Dot Product Visualization] - Interactive visualization of dot products + +==== Matrices and Transformations + +* *Tutorials*: + - https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/transformations.html[Scratchapixel: Transformations] - Detailed explanation of transformation matrices + - https://learnopengl.com/Getting-started/Transformations[LearnOpenGL: Transformations] - Practical guide to transformations in graphics + +* *Interactive Tools*: + - https://www.shadertoy.com/view/ltBXW3[ShaderToy: Matrix Transformations] - Interactive visualization of matrix transformations + - https://www.redblobgames.com/articles/transform/[Red Blob Games: Interactive Transformations] - Visual explanation of 2D transformations (concepts extend to 3D) + +==== Quaternions + +* *Tutorials*: + - https://www.youtube.com/watch?v=zjMuIxRvygQ[3Blue1Brown: Quaternions and 3D rotation] - Visual explanation of quaternions + - https://www.3dgep.com/understanding-quaternions/[Understanding Quaternions] - Practical guide with code examples + +* *Interactive Tools*: + - https://eater.net/quaternions[Quaternion Visualizer] - Interactive visualization of quaternion rotations + - https://www.shadertoy.com/view/lsl3RH[ShaderToy: Quaternion Rotation] - Interactive quaternion rotation visualization + +==== Coordinate Systems and Handedness + +* *Tutorials*: + - https://learnopengl.com/Getting-started/Coordinate-Systems[LearnOpenGL: Coordinate Systems] - Explanation of different coordinate systems in graphics + - https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/coordinate-systems.html[Scratchapixel: Coordinate Systems] - Detailed explanation with graphics + +* *References*: + - https://www.khronos.org/opengl/wiki/Coordinate_Transformations[OpenGL Wiki: Coordinate Transformations] - Reference for coordinate transformations + - https://docs.microsoft.com/en-us/windows/win32/direct3d9/coordinate-systems[Microsoft Docs: Coordinate Systems] - Explanation of left-handed vs. right-handed systems + + +==== GLM Library (Used in our examples) + +* *Documentation*: + - https://github.com/g-truc/glm/blob/master/manual.md[GLM Manual] - Official documentation for the GLM math library + - https://glm.g-truc.net/0.9.9/api/index.html[GLM API Documentation] - API reference + +* *Tutorials*: + - https://learnopengl.com/Getting-started/Transformations[LearnOpenGL: Transformations with GLM] - Practical guide to using GLM for transformations + - https://www.lighthouse3d.com/tutorials/glm-tutorial/[GLM Tutorial] - Tutorial on using GLM for graphics math + +==== Interactive Learning Tools + +* *Visualizations*: + - https://www.geogebra.org/3d[GeoGebra 3D Calculator] - Create and manipulate 3D objects and transformations + - https://www.shadertoy.com/[ShaderToy] - Experiment with shaders that use 3D math + +* *Practice Problems*: + - https://www.khanacademy.org/math/linear-algebra/vectors-and-spaces[Khan Academy: Vectors and Spaces] - Practice problems for vector math + - https://www.khanacademy.org/math/linear-algebra/matrix-transformations[Khan Academy: Matrix Transformations] - Practice problems for matrix transformations + +These resources should help you gain a deeper understanding of the mathematical concepts used in 3D graphics and camera systems. If you're struggling with a particular concept, try looking at multiple resources as different explanations might resonate better with your learning style. + +link:03_transformation_matrices.adoc[Next: Transformation Matrices] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc new file mode 100644 index 00000000..d54bf086 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/03_camera_implementation.adoc @@ -0,0 +1,483 @@ +:pp: {plus}{plus} + += Camera & Transformations: Camera Implementation + +== Camera Implementation + +Now that we understand the mathematical foundations, let's implement a flexible camera system for our Vulkan application. We'll create a camera class that can be used to navigate our 3D scenes. This implementation is designed for a general-purpose 3D application or game engine, and the concepts can be applied to various types of applications, from first-person games to architectural visualization tools. + +=== Camera Types + +There are several types of cameras commonly used in 3D applications: + +* *First-Person Camera*: Simulates viewing the world through the eyes of a character. +* *Third-Person Camera*: Follows a character from behind or another fixed position. +* *Orbit Camera*: Rotates around a fixed point, useful for object inspection. +* *Free Camera*: Allows unrestricted movement in all directions. + +For our implementation, we'll focus on a versatile camera that can be configured for different use cases. + +=== Camera Class Design + +Let's design a camera class that encapsulates the necessary functionality: + +[source,cpp] +---- +class Camera { +private: + // Camera position and orientation + glm::vec3 position; + glm::vec3 front; + glm::vec3 up; + glm::vec3 right; + glm::vec3 worldUp; + + // Euler angles + float yaw; + float pitch; + + // Camera options + float movementSpeed; + float mouseSensitivity; + float zoom; + + // Update camera vectors based on Euler angles + void updateCameraVectors(); + +public: + // Constructor with default values + Camera( + glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), + float yaw = -90.0f, + float pitch = 0.0f + ); + + // Get view matrix + glm::mat4 getViewMatrix() const; + + // Get projection matrix + glm::mat4 getProjectionMatrix(float aspectRatio, float nearPlane = 0.1f, float farPlane = 100.0f) const; + + // Process keyboard input for camera movement + void processKeyboard(CameraMovement direction, float deltaTime); + + // Process mouse movement for camera rotation + void processMouseMovement(float xOffset, float yOffset, bool constrainPitch = true); + + // Process mouse scroll for zoom + void processMouseScroll(float yOffset); + + // Getters for camera properties + glm::vec3 getPosition() const { return position; } + glm::vec3 getFront() const { return front; } + float getZoom() const { return zoom; } +}; +---- + +=== Camera Movement + +We'll define an enum for camera movement directions: + +[source,cpp] +---- +enum class CameraMovement { + FORWARD, + BACKWARD, + LEFT, + RIGHT, + UP, + DOWN +}; +---- + +And implement the movement logic: + +[source,cpp] +---- +void Camera::processKeyboard(CameraMovement direction, float deltaTime) { + float velocity = movementSpeed * deltaTime; + + if (direction == CameraMovement::FORWARD) + position += front * velocity; + if (direction == CameraMovement::BACKWARD) + position -= front * velocity; + if (direction == CameraMovement::LEFT) + position -= right * velocity; + if (direction == CameraMovement::RIGHT) + position += right * velocity; + if (direction == CameraMovement::UP) + position += up * velocity; + if (direction == CameraMovement::DOWN) + position -= up * velocity; +} +---- + +==== Handling Input Events + +The camera class provides methods to process input, but you'll need to connect these to your application's input system. Here's how you might capture keyboard and mouse input using GLFW, (a common windowing library used with Vulkan): + +[source,cpp] +---- +// In your application's input handling function +void processInput(GLFWwindow* window, Camera& camera, float deltaTime) { + // Keyboard input for camera movement + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::LEFT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::UP, deltaTime); + if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::DOWN, deltaTime); +} + +// Mouse callback function for camera rotation +void mouseCallback(GLFWwindow* window, double xpos, double ypos) { + static bool firstMouse = true; + static float lastX = 0.0f, lastY = 0.0f; + + if (firstMouse) { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + float xoffset = xpos - lastX; + float yoffset = lastY - ypos; // Reversed: y ranges bottom to top + + lastX = xpos; + lastY = ypos; + + // Pass the mouse movement to the camera + camera.processMouseMovement(xoffset, yoffset); +} + +// Scroll callback for zoom +void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + camera.processMouseScroll(yoffset); +} + +// Setting up the callbacks in your initialization code +void setupInputCallbacks(GLFWwindow* window) { + glfwSetCursorPosCallback(window, mouseCallback); + glfwSetScrollCallback(window, scrollCallback); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // Capture mouse +} +---- + +[NOTE] +==== +The specific implementation of input handling will depend on your windowing library and application architecture. The example above uses GLFW, but similar principles apply to other libraries like SDL, Qt, or platform-specific APIs. For more details on input handling with GLFW, refer to the https://www.glfw.org/docs/latest/input_guide.html[GLFW Input Guide]. +==== + +=== Camera Rotation + +For camera rotation, we'll use mouse input to adjust the yaw and pitch angles: + +[source,cpp] +---- +void Camera::processMouseMovement(float xOffset, float yOffset, bool constrainPitch) { + xOffset *= mouseSensitivity; + yOffset *= mouseSensitivity; + + yaw += xOffset; + pitch += yOffset; + + // Constrain pitch to avoid flipping + if (constrainPitch) { + if (pitch > 89.0f) + pitch = 89.0f; + if (pitch < -89.0f) + pitch = -89.0f; + } + + // Update camera vectors based on updated Euler angles + updateCameraVectors(); +} +---- + +=== Updating Camera Vectors + +After changing the camera's orientation, we need to recalculate the front, right, and up vectors: + +[source,cpp] +---- +void Camera::updateCameraVectors() { + // Calculate the new front vector + glm::vec3 newFront; + newFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); + newFront.y = sin(glm::radians(pitch)); + newFront.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); + front = glm::normalize(newFront); + + // Recalculate the right and up vectors + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +=== View Matrix + +The view matrix transforms world coordinates into view coordinates (camera space): + +[source,cpp] +---- +glm::mat4 Camera::getViewMatrix() const { + return glm::lookAt(position, position + front, up); +} +---- + +=== Projection Matrix + +The projection matrix transforms view coordinates into clip coordinates: + +[source,cpp] +---- +glm::mat4 Camera::getProjectionMatrix(float aspectRatio, float nearPlane, float farPlane) const { + return glm::perspective(glm::radians(zoom), aspectRatio, nearPlane, farPlane); +} +---- + +=== Advanced Topics: Third-Person Camera Implementation + +In this section, we'll explore advanced techniques for implementing a third-person camera that follows a character while avoiding occlusion and maintaining focus on the character. + +==== Third-Person Camera Design + +A third-person camera typically needs to: + +1. Follow the character at a specified distance +2. Maintain a consistent view of the character +3. Avoid being occluded by objects in the environment +4. Provide smooth transitions during movement and rotation + +Let's extend our camera class to support these features: + +[source,cpp] +---- +class ThirdPersonCamera : public Camera { +private: + // Target (character) properties + glm::vec3 targetPosition; + glm::vec3 targetForward; + + // Camera configuration + float followDistance; + float followHeight; + float followSmoothness; + + // Occlusion avoidance + float minDistance; + float raycastDistance; + + // Internal state + glm::vec3 desiredPosition; + glm::vec3 smoothDampVelocity; + +public: + ThirdPersonCamera( + float followDistance = 5.0f, + float followHeight = 2.0f, + float followSmoothness = 0.1f, + float minDistance = 1.0f + ); + + // Update camera position based on target + void updatePosition(const glm::vec3& targetPos, const glm::vec3& targetFwd, float deltaTime); + + // Handle occlusion avoidance + void handleOcclusion(const Scene& scene); + + // Orbit around target + void orbit(float horizontalAngle, float verticalAngle); + + // Setters for camera properties + void setFollowDistance(float distance) { followDistance = distance; } + void setFollowHeight(float height) { followHeight = height; } + void setFollowSmoothness(float smoothness) { followSmoothness = smoothness; } +}; +---- + +==== Character Following Algorithm + +The core of a third-person camera is the algorithm that positions the camera relative to the character. Here's an implementation of the `updatePosition` method: + +[source,cpp] +---- +void ThirdPersonCamera::updatePosition( + const glm::vec3& targetPos, + const glm::vec3& targetFwd, + float deltaTime +) { + // Update target properties + targetPosition = targetPos; + targetForward = glm::normalize(targetFwd); + + // Calculate the desired camera position + // Position the camera behind and above the character + glm::vec3 offset = -targetForward * followDistance; + offset.y = followHeight; + + desiredPosition = targetPosition + offset; + + // Smooth camera movement using exponential smoothing + position = glm::mix(position, desiredPosition, 1.0f - pow(followSmoothness, deltaTime * 60.0f)); + + // Update the camera to look at the target + front = glm::normalize(targetPosition - position); + + // Recalculate right and up vectors + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +This implementation: + +1. Positions the camera behind the character based on the character's forward direction +2. Adds height to give a better view of the character and surroundings +3. Uses exponential smoothing to create natural camera movement +4. Always keeps the camera focused on the character + +==== Occlusion Avoidance + +One of the most challenging aspects of a third-person camera is handling occlusion—when objects in the environment block the view of the character. Here's an implementation of occlusion avoidance: + +[source,cpp] +---- +void ThirdPersonCamera::handleOcclusion(const Scene& scene) { + // Cast a ray from the target to the desired camera position + Ray ray; + ray.origin = targetPosition; + ray.direction = glm::normalize(desiredPosition - targetPosition); + + // Check for intersections with scene objects + RaycastHit hit; + if (scene.raycast(ray, hit, glm::length(desiredPosition - targetPosition))) { + // If there's an intersection, move the camera to the hit point + // minus a small offset to avoid clipping + float offsetDistance = 0.2f; + position = hit.point - (ray.direction * offsetDistance); + + // Ensure we don't get too close to the target + float currentDistance = glm::length(position - targetPosition); + if (currentDistance < minDistance) { + position = targetPosition + ray.direction * minDistance; + } + + // Update the camera to look at the target + front = glm::normalize(targetPosition - position); + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); + } +} +---- + +This implementation: + +1. Casts a ray from the character to the desired camera position +2. If the ray hits an object, moves the camera to the hit point (with a small offset) +3. Ensures the camera doesn't get too close to the character +4. Updates the camera orientation to maintain focus on the character + +===== Performance Considerations for Occlusion Avoidance + +When implementing occlusion avoidance, be mindful of performance: + +* *Use simplified collision geometry*: For raycasting, use simpler collision shapes than your rendering geometry +* *Limit the frequency of occlusion checks*: You may not need to check every frame on slower devices +* *Consider spatial partitioning*: Use structures like octrees to speed up raycasts by quickly eliminating objects that can't possibly intersect with the ray +* *Optimize for mobile platforms*: For performance-constrained devices, consider simplifying the occlusion algorithm or reducing its precision + +==== Implementing Orbit Controls + +Many third-person games allow the player to orbit the camera around the character. Here's how to implement this functionality: + +[source,cpp] +---- +void ThirdPersonCamera::orbit(float horizontalAngle, float verticalAngle) { + // Update yaw and pitch based on input + yaw += horizontalAngle; + pitch += verticalAngle; + + // Constrain pitch to avoid flipping + if (pitch > 89.0f) + pitch = 89.0f; + if (pitch < -89.0f) + pitch = -89.0f; + + // Calculate the new camera position based on spherical coordinates + float radius = followDistance; + float yawRad = glm::radians(yaw); + float pitchRad = glm::radians(pitch); + + // Convert spherical coordinates to Cartesian + glm::vec3 offset; + offset.x = radius * cos(yawRad) * cos(pitchRad); + offset.y = radius * sin(pitchRad); + offset.z = radius * sin(yawRad) * cos(pitchRad); + + // Set the desired position + desiredPosition = targetPosition + offset; + + // Update camera vectors + front = glm::normalize(targetPosition - desiredPosition); + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +This implementation: + +1. Updates the camera's yaw and pitch based on user input +2. Constrains the pitch to prevent the camera from flipping +3. Calculates a new camera position using spherical coordinates +4. Keeps the camera focused on the character + +==== Integrating with Character Movement + +To create a complete third-person camera system, we need to integrate it with character movement. Here's an example of how to use the third-person camera in a game loop: + +[source,cpp] +---- +void gameLoop(float deltaTime) { + // Update character position and orientation based on input + character.update(deltaTime); + + // Update camera position to follow the character + thirdPersonCamera.updatePosition( + character.getPosition(), + character.getForward(), + deltaTime + ); + + // Handle camera occlusion + thirdPersonCamera.handleOcclusion(scene); + + // Process camera orbit input (if any) + if (mouseInputDetected) { + thirdPersonCamera.orbit(mouseDeltaX, mouseDeltaY); + } + + // Get the view and projection matrices for rendering + glm::mat4 viewMatrix = thirdPersonCamera.getViewMatrix(); + glm::mat4 projMatrix = thirdPersonCamera.getProjectionMatrix(aspectRatio); + + // Use these matrices for rendering the scene + renderer.render(scene, viewMatrix, projMatrix); +} +---- + +[NOTE] +==== +For more advanced camera techniques, refer to the Advanced Camera Techniques section in the Appendix. +==== + +In the next section, we'll explore how to use transformation matrices to position objects in our 3D scene. + +link:04_transformation_matrices.adoc[Next: Transformation Matrices] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc new file mode 100644 index 00000000..f1cd791a --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/03_transformation_matrices.adoc @@ -0,0 +1,176 @@ +:pp: {plus}{plus} + += Camera & Transformations: Transformation Matrices + +== Transformation Matrices + +In this section, we'll dive deeper into the transformation matrices used in 3D graphics and how they're applied in our rendering pipeline. + +=== The Model-View-Projection (MVP) Pipeline + +The transformation of vertices from object space to screen space involves a series of matrix multiplications, commonly known as the MVP pipeline: + +[source,cpp] +---- +// The complete transformation pipeline +glm::mat4 MVP = projectionMatrix * viewMatrix * modelMatrix; +---- + +Let's explore each of these matrices in detail. + +=== Model Matrix + +The model matrix transforms vertices from object space to world space. It positions, rotates, and scales objects in the world. + +[source,cpp] +---- +glm::mat4 createModelMatrix( + const glm::vec3& position, + const glm::vec3& rotation, + const glm::vec3& scale +) { + // Start with identity matrix + glm::mat4 model = glm::mat4(1.0f); + + // Apply transformations in order: scale, rotate, translate + model = glm::translate(model, position); + + // Apply rotations around each axis + model = glm::rotate(model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + model = glm::rotate(model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + model = glm::rotate(model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + + // Apply scaling + model = glm::scale(model, scale); + + return model; +} +---- + +=== View Matrix + +The view matrix transforms vertices from world space to view space (camera space). It represents the position and orientation of the camera. + +[source,cpp] +---- +glm::mat4 createViewMatrix( + const glm::vec3& cameraPosition, + const glm::vec3& cameraTarget, + const glm::vec3& upVector +) { + return glm::lookAt(cameraPosition, cameraTarget, upVector); +} +---- + +The `lookAt` function creates a view matrix that positions the camera at `cameraPosition`, looking at `cameraTarget`, with `upVector` defining the up direction. + +=== Projection Matrix + +The projection matrix transforms vertices from view space to clip space. It defines how 3D coordinates are projected onto the 2D screen. + +==== Perspective Projection + +Perspective projection simulates how objects appear smaller as they get farther away, which is how our eyes naturally perceive the world. + +[source,cpp] +---- +glm::mat4 createPerspectiveMatrix( + float fovY, + float aspectRatio, + float nearPlane, + float farPlane +) { + return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane); +} +---- + +Parameters: + +* `fovY`: Field of view angle in degrees (vertical) +* `aspectRatio`: Width divided by height of the viewport +* `nearPlane`: Distance to the near clipping plane +* `farPlane`: Distance to the far clipping plane + +==== Orthographic Projection + +Orthographic projection doesn't have perspective distortion, making it useful for 2D rendering or technical drawings. + +[source,cpp] +---- +glm::mat4 createOrthographicMatrix( + float left, + float right, + float bottom, + float top, + float nearPlane, + float farPlane +) { + return glm::ortho(left, right, bottom, top, nearPlane, farPlane); +} +---- + +=== Normal Matrix + +When applying non-uniform scaling to objects, normals can become incorrect if transformed with the model matrix. The normal matrix solves this issue: + +[source,cpp] +---- +glm::mat3 createNormalMatrix(const glm::mat4& modelMatrix) { + // The normal matrix is the transpose of the inverse of the upper-left 3x3 part of the model matrix + return glm::transpose(glm::inverse(glm::mat3(modelMatrix))); +} +---- + +=== Applying Transformations in Shaders + +In Vulkan, we typically pass these matrices to our shaders as uniform variables: + +[source,glsl] +---- +// Vertex shader +#version 450 + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inTexCoord; + +layout(location = 0) out vec3 fragNormal; +layout(location = 1) out vec2 fragTexCoord; + +void main() { + // Apply MVP transformation + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); + + // Transform normal using normal matrix + mat3 normalMatrix = transpose(inverse(mat3(ubo.model))); + fragNormal = normalMatrix * inNormal; + + fragTexCoord = inTexCoord; +} +---- + +=== Hierarchical Transformations + +For complex objects or scenes with parent-child relationships, we use hierarchical transformations: + +[source,cpp] +---- +// Parent transformation +glm::mat4 parentModel = createModelMatrix(parentPosition, parentRotation, parentScale); + +// Child transformation relative to parent +glm::mat4 localModel = createModelMatrix(childLocalPosition, childLocalRotation, childLocalScale); + +// Combined transformation +glm::mat4 childWorldModel = parentModel * localModel; +---- + +In the next section, we'll implement a camera system that uses these transformation concepts to navigate our 3D scenes. + +link:04_camera_implementation.adoc[Next: Camera Implementation] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc new file mode 100644 index 00000000..a1ea6b14 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/04_camera_implementation.adoc @@ -0,0 +1,692 @@ +:pp: {plus}{plus} + += Camera & Transformations: Camera Implementation + +== Camera Implementation + +Now that we understand the mathematical foundations and transformation matrices, let's implement a flexible camera system for our Vulkan application. We'll create a camera class that can be used to navigate our 3D scenes. This implementation is designed for a general-purpose 3D application or game engine, and the concepts can be applied to various types of applications, from first-person games to architectural visualization tools. + +=== Camera Types + +There are several types of cameras commonly used in 3D applications: + +* *First-Person Camera*: Simulates viewing the world through the eyes of a character. +* *Third-Person Camera*: Follows a character from behind or another fixed position. +* *Orbit Camera*: Rotates around a fixed point, useful for object inspection. +* *Free Camera*: Allows unrestricted movement in all directions. + +For our implementation, we'll focus on a versatile camera that can be configured for different use cases. + +=== Camera Class Design + +Our camera system is built around a Camera class that manages 3D navigation and view generation. Let's break down the implementation into logical sections to understand both the technical details and design decisions behind each component. + +=== Camera Architecture: Core Data Members and Spatial Representation + +First, we establish the fundamental data structures that represent the camera's position, orientation, and coordinate system within 3D space. + +[source,cpp] +---- +class Camera { +private: + // Spatial positioning and orientation vectors + // These form the camera's local coordinate system in world space + glm::vec3 position; // Camera's location in world coordinates + glm::vec3 front; // Forward direction (where camera is looking) + glm::vec3 up; // Camera's local up direction (for roll control) + glm::vec3 right; // Camera's local right direction (perpendicular to front and up) + glm::vec3 worldUp; // Global up vector reference (typically Y-axis) +---- + +The spatial representation uses a right-handed coordinate system where the camera maintains its own local coordinate frame within the world space. The `position` vector defines where the camera exists, while `front`, `up`, and `right` vectors form an orthonormal basis that defines the camera's orientation. This approach provides intuitive control where moving along the `front` vector moves the camera forward, `right` moves sideways, and `up` moves vertically relative to the camera's current orientation. + +The `worldUp` vector serves as a reference point for maintaining proper orientation, typically pointing along the world's Y-axis. This reference prevents the camera from becoming disoriented during complex rotations and ensures that operations like "level the horizon" have a consistent reference point. + +=== Camera Architecture: Euler Angle Representation and Control Parameters + +Next, we define how rotations are represented and controlled, using Euler angles for intuitive user input while managing the mathematical complexities internally. + +[source,cpp] +---- + // Rotation representation using Euler angles + // Provides intuitive control while managing gimbal lock and other rotation complexities + float yaw; // Horizontal rotation around the world up-axis (left-right looking) + float pitch; // Vertical rotation around the camera's right axis (up-down looking) + + // User interaction and behavior parameters + // These control how the camera responds to input and environmental factors + float movementSpeed; // Units per second for translation movement + float mouseSensitivity; // Multiplier for mouse input to rotation angle conversion + float zoom; // Field of view control for perspective projection +---- + +Euler angles provide an intuitive interface for camera rotation that maps naturally to user input devices. Yaw controls horizontal rotation (looking left-right), while pitch controls vertical rotation (looking up-down). We deliberately avoid roll for most applications as it can be disorienting for users, though the system could be extended to support it. + +The parameter system allows fine-tuning of camera behavior for different use cases. Movement speed can be adjusted for different scene scales, mouse sensitivity can accommodate user preferences and different input devices, and zoom provides dynamic field-of-view control for gameplay or cinematic effects. + +=== Camera Architecture: Internal Methods and State Management + +Next, we define the internal methods responsible for maintaining mathematical consistency and updating the camera's coordinate system when rotations change. + +[source,cpp] +---- + // Internal coordinate system maintenance + // Ensures mathematical consistency when orientation changes occur + void updateCameraVectors(); + +public: +---- + +The `updateCameraVectors` method serves as the mathematical foundation of the camera system, recalculating the `front`, `right`, and `up` vectors whenever the Euler angles change. This process involves trigonometric calculations that convert the intuitive Euler angle representation into the orthonormal vector basis required for matrix operations and movement calculations. + +This approach separates the user-friendly angle interface from the computationally efficient vector operations, allowing the camera to present simple controls while maintaining the mathematical rigor required for accurate 3D transformations. + +=== Camera Architecture: Public Interface and Constructor Design + +Next, we establish the public interface that external code uses to create, configure, and interact with camera instances. + +[source,cpp] +---- + // Constructor with sensible defaults for common use cases + // Provides flexibility while ensuring the camera starts in a predictable state + Camera( + glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), // Start at world origin + glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), // Y-axis as world up + float yaw = -90.0f, // Look along negative Z-axis (OpenGL convention) + float pitch = 0.0f // Level horizon + ); +---- + +The constructor design reflects common 3D graphics conventions and practical defaults. The default position at the origin provides a predictable starting point, while the Y-axis world up aligns with the standard mathematical coordinate system. The initial yaw of -90° follows OpenGL conventions where the default view looks down the negative Z-axis, creating a right-handed coordinate system that feels natural to users. + +The parameter defaults eliminate the need for complex initialization in simple use cases while still allowing full customization when needed for specialized applications. + +=== Camera Architecture: Matrix Generation and Geometric Transformation Interface + +Now we define the core mathematical interface that transforms the camera's spatial representation into the matrices required by graphics pipelines. + +[source,cpp] +---- + // Matrix generation for graphics pipeline integration + // These methods bridge between the camera's spatial representation and GPU requirements + glm::mat4 getViewMatrix() const; + glm::mat4 getProjectionMatrix(float aspectRatio, float nearPlane = 0.1f, float farPlane = 100.0f) const; +---- + +The matrix generation methods serve as the critical bridge between our intuitive camera representation and the mathematical requirements of 3D graphics pipelines. The view matrix transforms world coordinates into camera space, effectively positioning the world relative to the camera's viewpoint. The projection matrix then transforms camera space into clip space, handling perspective effects and preparing coordinates for rasterization. + +The separation of view and projection matrices follows standard graphics pipeline architecture, allowing independent control over camera positioning and perspective characteristics. This design enables techniques like changing field-of-view for zoom effects without recalculating the camera's spatial relationships. + +=== Camera Architecture: Input Processing and User Interaction + +Finally, let's define how the camera responds to various forms of user input, providing the interface between human interaction and camera movement. + +[source,cpp] +---- + // Input processing methods for different interaction modalities + // Each method handles a specific type of user input with appropriate transformations + void processKeyboard(CameraMovement direction, float deltaTime); // Keyboard-based translation + void processMouseMovement(float xOffset, float yOffset, bool constrainPitch = true); // Mouse-based rotation + void processMouseScroll(float yOffset); // Scroll-based zoom control + + // Property access methods for external systems + // Provide controlled access to internal state without exposing implementation details + glm::vec3 getPosition() const { return position; } + glm::vec3 getFront() const { return front; } + float getZoom() const { return zoom; } +}; +---- + +The input processing architecture recognizes that different input modalities serve different purposes in camera control. Keyboard input typically handles discrete directional movement, mouse movement provides continuous rotation control, and scroll wheels offer intuitive zoom adjustment. Each method is designed to handle its specific input type with appropriate mathematical transformations and timing considerations. + +The getter methods provide controlled access to internal state, allowing external systems (like audio systems that need listener position, or culling systems that need view direction) to access camera properties without exposing the internal implementation details or allowing uncontrolled modification of the camera's state. + +=== Camera Movement + +We'll define an enum for camera movement directions: + +[source,cpp] +---- +enum class CameraMovement { + FORWARD, + BACKWARD, + LEFT, + RIGHT, + UP, + DOWN +}; +---- + +And implement the movement logic: + +[source,cpp] +---- +void Camera::processKeyboard(CameraMovement direction, float deltaTime) { + float velocity = movementSpeed * deltaTime; + + if (direction == CameraMovement::FORWARD) + position += front * velocity; + if (direction == CameraMovement::BACKWARD) + position -= front * velocity; + if (direction == CameraMovement::LEFT) + position -= right * velocity; + if (direction == CameraMovement::RIGHT) + position += right * velocity; + if (direction == CameraMovement::UP) + position += up * velocity; + if (direction == CameraMovement::DOWN) + position -= up * velocity; +} +---- + +==== Handling Input Events + +The camera class provides methods to process input, but integrating these with your application's input system requires careful consideration of different input modalities and their unique characteristics. Let's break down the input handling implementation to demonstrate both the technical integration and the design principles behind effective camera controls. + +=== Input Integration: Keyboard Input Processing and Movement Translation + +First, we handle discrete directional input from keyboards, translating key presses into camera movement commands with proper frame-rate independence. + +[source,cpp] +---- +// Keyboard input processing for camera translation +// Handles discrete directional commands with frame-rate independent timing +void processInput(GLFWwindow* window, Camera& camera, float deltaTime) { + // WASD movement scheme following standard FPS conventions + // Each key press translates to a specific directional movement relative to camera orientation + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); // Move forward along camera's front vector + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); // Move backward opposite to front vector + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::LEFT, deltaTime); // Strafe left along camera's right vector + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); // Strafe right along camera's right vector + + // Vertical movement controls for 3D navigation + // Space and Control provide intuitive up/down movement + if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::UP, deltaTime); // Move up along camera's up vector + if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::DOWN, deltaTime); // Move down opposite to up vector +} +---- + +The keyboard input processing follows established conventions from first-person games, where WASD keys control horizontal movement and Space/Control handle vertical movement. This mapping feels intuitive to users and provides complete 6-degrees-of-freedom movement control. The frame-rate independence achieved through deltaTime ensures consistent movement speed regardless of rendering performance, which is crucial for predictable user experience across different hardware configurations. + +Each movement command uses the camera's local coordinate system rather than world coordinates. Meaning "forward" always moves in the direction the camera is facing, "right" moves perpendicular to the view direction, and "up" moves along the camera's local vertical axis. This approach provides intuitive controls that respond naturally to camera orientation changes. + +=== Input Integration: Mouse Movement Processing and Rotation State Management + +Now, let's handle continuous mouse input for camera rotation, managing state persistence and coordinate system conversions for smooth camera control. + +[source,cpp] +---- +// Mouse movement callback for continuous camera rotation +// Manages state persistence and coordinate transformations for smooth rotation control +void mouseCallback(GLFWwindow* window, double xpos, double ypos) { + // State persistence for calculating movement deltas + // Static variables maintain state between callback invocations + static bool firstMouse = true; // Flag to handle initial mouse position + static float lastX = 0.0f, lastY = 0.0f; // Previous mouse position for delta calculation + + // Handle initial mouse position to prevent sudden camera jumps + // First callback provides absolute position, not relative movement + if (firstMouse) { + lastX = xpos; // Initialize previous position + lastY = ypos; + firstMouse = false; // Disable special handling for subsequent calls + } + + // Calculate mouse movement deltas since last callback + // These deltas represent the amount and direction of mouse movement + float xoffset = xpos - lastX; // Horizontal movement (left-right) + float yoffset = lastY - ypos; // Vertical movement (inverted: screen Y increases downward, camera pitch increases upward) + + // Update state for next callback iteration + lastX = xpos; + lastY = ypos; + + // Convert mouse movement to camera rotation + // Delta values drive continuous camera orientation changes + camera.processMouseMovement(xoffset, yoffset); +} +---- + +The mouse callback demonstrates the complexities of handling continuous input in event-driven systems. The static variables maintain state between callback invocations, which is necessary because mouse movement is reported as absolute positions rather than relative deltas. The first-mouse handling prevents jarring camera jumps when the mouse cursor is first captured. + +The Y-axis inversion (`lastY - ypos`) addresses the coordinate system mismatch between screen space (where Y increases downward) and camera space (where positive pitch looks upward). This inversion ensures that moving the mouse upward rotates the camera to look up, matching user expectations from other 3D applications. + +=== Input Integration: Scroll Input Processing and Zoom Control + +Next, let's work on the scroll-wheel input to give us zoom control, providing a simple interface for field-of-view adjustments that feel natural to users. + +[source,cpp] +---- +// Scroll wheel callback for zoom control +// Provides intuitive field-of-view adjustment through scroll wheel interaction +void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + // Direct scroll-to-zoom mapping + // Positive yoffset (scroll up) typically zooms in, negative (scroll down) zooms out + camera.processMouseScroll(yoffset); +} +---- + +The scroll callback maintains simplicity by directly passing the scroll delta to the camera's zoom processing method. This design delegates the mathematical details of zoom control to the camera class while providing a clean interface for scroll wheel events. The scroll direction convention (positive for zoom in, negative for zoom out) follows standard user interface patterns. + +=== Input Integration: System Integration and Input Mode Configuration + +Finally, we establish the integration between the input callbacks and the windowing system, configuring mouse capture and callback registration for complete camera control. + +[source,cpp] +---- +// Input system initialization and callback registration +// Establishes the connection between windowing system and camera control callbacks +void setupInputCallbacks(GLFWwindow* window) { + // Register callback functions with the windowing system + // These establish the event-driven connection between hardware input and camera control + glfwSetCursorPosCallback(window, mouseCallback); // Connect mouse movement to camera rotation + glfwSetScrollCallback(window, scrollCallback); // Connect scroll wheel to camera zoom + + // Configure mouse capture mode for first-person camera behavior + // Disabling the cursor provides continuous mouse input without cursor interference + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); +} +---- + +The system integration demonstrates how camera controls integrate with the broader application architecture. The callback registration creates the event-driven connection between hardware input and camera responses, while the cursor disabling provides the seamless mouse control expected in 3D applications. + +The `GLFW_CURSOR_DISABLED` mode captures the mouse cursor, allowing unlimited mouse movement without the cursor hitting screen boundaries. This configuration is essential for first-person camera controls where users expect to be able to turn the camera continuously in any direction without cursor limitations. + +[NOTE] +==== +The specific implementation of input handling will depend on your windowing library and application architecture. The example above uses GLFW, but similar principles apply to other libraries like SDL, Qt, or platform-specific APIs. For more details on input handling with GLFW, refer to the link:https://www.glfw.org/docs/latest/input_guide.html[GLFW Input Guide]. +==== + +=== Camera Rotation + +For camera rotation, we'll use mouse input to adjust the yaw and pitch angles: + +[source,cpp] +---- +void Camera::processMouseMovement(float xOffset, float yOffset, bool constrainPitch) { + xOffset *= mouseSensitivity; + yOffset *= mouseSensitivity; + + yaw += xOffset; + pitch += yOffset; + + // Constrain pitch to avoid flipping + if (constrainPitch) { + if (pitch > 89.0f) + pitch = 89.0f; + if (pitch < -89.0f) + pitch = -89.0f; + } + + // Update camera vectors based on updated Euler angles + updateCameraVectors(); +} +---- + +=== Updating Camera Vectors + +After changing the camera's orientation, we need to recalculate the front, right, and up vectors: + +[source,cpp] +---- +void Camera::updateCameraVectors() { + // Calculate the new front vector + glm::vec3 newFront; + newFront.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); + newFront.y = sin(glm::radians(pitch)); + newFront.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); + front = glm::normalize(newFront); + + // Recalculate the right and up vectors + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +=== View Matrix + +The view matrix transforms world coordinates into view coordinates (camera space): + +[source,cpp] +---- +glm::mat4 Camera::getViewMatrix() const { + return glm::lookAt(position, position + front, up); +} +---- + +=== Projection Matrix + +The projection matrix transforms view coordinates into clip coordinates: + +[source,cpp] +---- +glm::mat4 Camera::getProjectionMatrix(float aspectRatio, float nearPlane, float farPlane) const { + return glm::perspective(glm::radians(zoom), aspectRatio, nearPlane, farPlane); +} +---- + +=== Advanced Topics: Third-Person Camera Implementation + +In this section, we'll explore advanced techniques for implementing a third-person camera that follows a character while avoiding occlusion and maintaining focus on the character. + +==== Third-Person Camera Design + +A third-person camera typically needs to: + +1. Follow the character at a specified distance +2. Maintain a consistent view of the character +3. Avoid being occluded by objects in the environment +4. Provide smooth transitions during movement and rotation + +Let's extend our camera class to support these features by building a specialized ThirdPersonCamera that addresses the unique challenges of the character-following camera systems. + +=== Third-Person Camera Architecture: Target Tracking and Spatial Relationship Management + +What good is a camera if we can't use it to target looking at things? Maybe we also want characters to look at each other or to have them look at the camera. Let's start work on this by figuring out how a 'lookat' system would work and how a camera would track a target. + +The getter methods provide controlled access to internal state, allowing external systems (like audio systems that need listener position, or culling systems that need a view direction) to query the camera without tightly coupling to the implementation. This keeps the camera easy to maintain and extend as features are added. + +==== Look-At Basics: Pointing the Camera at Something + +Before we automate camera behaviors, let’s build an intuition for “look-at.” The idea is simple: given we know where the camera is: "the eye," we also know what point it should face: "the target," and we also know which way is "up." We want to use that information to construct an orientation that makes the camera face the target while keeping the horizon stable. + +Think of it like lining up a real camera: + +- Eye: “Where am I standing?” +- Target: “What am I framing in the center of the viewfinder?” +- Up: “Which direction should the top of the frame point (so the picture isn’t tilted)?” + +Thus, when we get to the output of "look at," we will have a view orientation. We usually for convenience will use an affine matrix, but it is only an orientation. After all, rotating to "look at" something shouldn't involve translating to a new position; so the eye will maintain the position throughout our look-at code. + +Key takeaways: + +- “Look-at” defines an orientation, not a position. The position comes from the eye; look-at figures out the directions (forward/right/up) from eye→target and the chosen up. +- The up direction should not be parallel to the eye→target direction. If they’re nearly aligned, the camera won’t know how to keep the horizon level (it can “roll unpredictably.”) +- You can use look-at for both cameras and objects. Characters can face each other, or you can point a spotlight or turret at a target with the same concept. + +In the next section, we’ll take this one-off “point at a target” idea and turn it into a behavior: smooth, continuous camera target tracking that follows moving subjects without jitter or sudden snaps. + +=== Implementation for camera target relationship + +First, establish the fundamental relationship between the camera and its target, managing the spatial tracking information that drives all third-person camera behaviors. + +[source,cpp] +---- +class ThirdPersonCamera : public Camera { +private: + // Target entity tracking and spatial relationship data + // These properties define the relationship between camera and the character being followed + glm::vec3 targetPosition; // Current world position of the target character + glm::vec3 targetForward; // Target's forward direction vector for contextual camera positioning +---- + +The target tracking system forms the foundation of third-person camera behavior by maintaining a continuous connection between the camera and the character being followed. The `targetPosition` provides the spatial anchor that the camera revolves around, while `targetForward` enables context-aware camera positioning that can anticipate where the character is moving or looking. + +This approach allows the camera to make intelligent positioning decisions based on the character's state and orientation, creating more dynamic and responsive camera behavior than simple fixed-offset following. + +=== Third-Person Camera Architecture: Behavioral Configuration and Control Parameters + +Now let's work on the parameters that control how the camera behaves in relation to its target, providing artistic and gameplay control over the camera's characteristics. + +[source,cpp] +---- + // Camera behavior configuration parameters + // These values control the aesthetic and functional characteristics of camera following + float followDistance; // Desired distance from target (affects intimacy and field of view) + float followHeight; // Height offset above target (provides better scene visibility) + float followSmoothness; // Interpolation factor for smooth camera transitions (0 = instant, 1 = never) +---- + +The behavioral parameters provide artistic control over the camera's personality and functional characteristics. Follow distance affects both the visual intimacy with the character and the amount of surrounding environment visible in the frame. Height offset ensures the camera provides good visibility of both the character and the surrounding terrain or obstacles. + +The smoothness parameter controls the camera's responsiveness to target movement, allowing designers to balance between immediate response, (which can feel jerky,) and smooth motion (which can feel laggy). This parameter is crucial for creating camera behavior that feels natural and responsive to different gameplay situations. + +=== Third-Person Camera Architecture: Collision Detection and Occlusion Management + +Now, we have a camera system that will work in basic situations. However, let's briefly talk about the complex problem of environmental occlusion, ensuring the camera maintains visibility of the target even when obstacles interfere with the desired positioning. + +[source,cpp] +---- + // Occlusion avoidance and collision management + // These parameters control how the camera responds to environmental obstacles + float minDistance; // Minimum allowed distance from target (prevents camera from getting too close) + float raycastDistance; // Maximum distance for occlusion detection rays +---- + +The occlusion management system addresses one of the most challenging aspects of third-person camera implementation: maintaining visibility when environmental geometry interferes with the desired camera position. The minimum distance prevents the camera from getting uncomfortably close to the character during collision situations, while the raycast distance defines how far the camera looks ahead for potential occlusion issues. + +This system enables the camera to proactively respond to environmental constraints, smoothly adjusting its position to maintain optimal visibility without jarring transitions or sudden position changes that can be disorienting to players. + +=== Third-Person Camera Architecture: Internal State Management and Motion Control + +To get smooth camera motion, we need to be able to understand the FSM (Finite State Machine) design of the Camera architecture. We manage the internal computational state required for intelligent positioning decisions and to help solve smooth camera motion. + +[source,cpp] +---- + // Internal computational state for smooth motion control + // These variables manage the mathematical aspects of camera positioning and movement + glm::vec3 desiredPosition; // Target position the camera wants to reach (before collision adjustments) + glm::vec3 smoothDampVelocity; // Velocity state for smooth damping interpolation algorithms + +public: +---- + +The internal state management separates the desired camera behavior from the actual camera position, allowing the system to handle complex scenarios where multiple forces influence camera positioning. The desired position represents where the camera would ideally be placed based on the follow parameters, while the smooth damp velocity enables sophisticated interpolation algorithms that create natural, physics-inspired camera motion. + +This separation of concerns allows the camera system to handle conflicts between desired positioning and environmental constraints gracefully, maintaining smooth motion even when the camera must deviate significantly from its preferred location. + +=== Third-Person Camera Architecture: Public Interface and Configuration Control + +Now, let's examine the external interface that allows game code to interact with and configure the third-person camera system in a manner that can avoid tight coupling and can keep the camera as its' own module. + +[source,cpp] +---- + // Constructor with gameplay-tuned defaults + // Default values chosen for common third-person game scenarios + ThirdPersonCamera( + float followDistance = 5.0f, // Medium distance providing good character visibility and environment context + float followHeight = 2.0f, // Height above target for clear sightlines over low obstacles + float followSmoothness = 0.1f, // Moderate smoothing for responsive but stable camera motion + float minDistance = 1.0f // Minimum distance to prevent uncomfortable close-ups + ); + + // Core functionality methods for camera behavior control + void updatePosition(const glm::vec3& targetPos, const glm::vec3& targetFwd, float deltaTime); + void handleOcclusion(const Scene& scene); + void orbit(float horizontalAngle, float verticalAngle); + + // Runtime configuration methods for dynamic camera adjustment + void setFollowDistance(float distance) { followDistance = distance; } + void setFollowHeight(float height) { followHeight = height; } + void setFollowSmoothness(float smoothness) { followSmoothness = smoothness; } +}; +---- + +The public interface design balances ease of use with powerful functionality, providing sensible defaults that work well for common third-person scenarios while allowing full customization when needed. The default values are chosen based on common third-person game requirements: medium distance for good character visibility, moderate height for environmental awareness, and balanced smoothing for responsive yet stable motion. + +The method organization separates the core update functionality (which typically runs every frame) from configuration methods (which are called less frequently) and specialized behaviors like orbiting (which might be triggered by specific user input). This design makes it easy to integrate the camera into different game loop architectures while maintaining a clear separation of concerns. + +==== Character Following Algorithm + +The core of a third-person camera is the algorithm that positions the camera relative to the character. Here's an implementation of the `updatePosition` method: + +[source,cpp] +---- +void ThirdPersonCamera::updatePosition( + const glm::vec3& targetPos, + const glm::vec3& targetFwd, + float deltaTime +) { + // Update target properties + targetPosition = targetPos; + targetForward = glm::normalize(targetFwd); + + // Calculate the desired camera position + // Position the camera behind and above the character + glm::vec3 offset = -targetForward * followDistance; + offset.y = followHeight; + + desiredPosition = targetPosition + offset; + + // Smooth camera movement using exponential smoothing + position = glm::mix(position, desiredPosition, 1.0f - pow(followSmoothness, deltaTime * 60.0f)); + + // Update the camera to look at the target + front = glm::normalize(targetPosition - position); + + // Recalculate right and up vectors + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +This implementation: + +1. Positions the camera behind the character based on the character's forward direction +2. Adds height to give a better view of the character and surroundings +3. Uses exponential smoothing to create natural camera movement +4. Always keeps the camera focused on the character + +==== Occlusion Avoidance + +One of the most challenging aspects of a third-person camera is handling occlusion - when objects in the environment block the view of the character. Here's an implementation of occlusion avoidance: + +[source,cpp] +---- +void ThirdPersonCamera::handleOcclusion(const Scene& scene) { + // Cast a ray from the target to the desired camera position + Ray ray; + ray.origin = targetPosition; + ray.direction = glm::normalize(desiredPosition - targetPosition); + + // Check for intersections with scene objects + RaycastHit hit; + if (scene.raycast(ray, hit, glm::length(desiredPosition - targetPosition))) { + // If there's an intersection, move the camera to the hit point + // minus a small offset to avoid clipping + float offsetDistance = 0.2f; + position = hit.point - (ray.direction * offsetDistance); + + // Ensure we don't get too close to the target + float currentDistance = glm::length(position - targetPosition); + if (currentDistance < minDistance) { + position = targetPosition + ray.direction * minDistance; + } + + // Update the camera to look at the target + front = glm::normalize(targetPosition - position); + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); + } +} +---- + +This implementation: + +1. Casts a ray from the character to the desired camera position +2. If the ray hits an object, moves the camera to the hit point (with a small offset) +3. Ensures the camera doesn't get too close to the character +4. Updates the camera orientation to maintain focus on the character + +===== Performance Considerations for Occlusion Avoidance + +When implementing occlusion avoidance, be mindful of performance: + +* *Use simplified collision geometry*: For raycasting, use simpler collision shapes than your rendering geometry +* *Limit the frequency of occlusion checks*: You may not need to check every frame on slower devices +* *Consider spatial partitioning*: Use structures like octrees to accelerate raycasts by quickly eliminating objects that can't possibly intersect with the ray +* *Optimize for mobile platforms*: For performance-constrained devices, consider simplifying the occlusion algorithm or reducing its precision + +==== Implementing Orbit Controls + +Many third-person games allow the player to orbit the camera around the character. Here's how to implement this functionality: + +[source,cpp] +---- +void ThirdPersonCamera::orbit(float horizontalAngle, float verticalAngle) { + // Update yaw and pitch based on input + yaw += horizontalAngle; + pitch += verticalAngle; + + // Constrain pitch to avoid flipping + if (pitch > 89.0f) + pitch = 89.0f; + if (pitch < -89.0f) + pitch = -89.0f; + + // Calculate the new camera position based on spherical coordinates + float radius = followDistance; + float yawRad = glm::radians(yaw); + float pitchRad = glm::radians(pitch); + + // Convert spherical coordinates to Cartesian + glm::vec3 offset; + offset.x = radius * cos(yawRad) * cos(pitchRad); + offset.y = radius * sin(pitchRad); + offset.z = radius * sin(yawRad) * cos(pitchRad); + + // Set the desired position + desiredPosition = targetPosition + offset; + + // Update camera vectors + front = glm::normalize(targetPosition - desiredPosition); + right = glm::normalize(glm::cross(front, worldUp)); + up = glm::normalize(glm::cross(right, front)); +} +---- + +This implementation: + +1. Updates the camera's yaw and pitch based on user input +2. Constrains the pitch to prevent the camera from flipping +3. Calculates a new camera position using spherical coordinates +4. Keeps the camera focused on the character + +==== Integrating with Character Movement + +To create a complete third-person camera system, we need to integrate it with character movement. Here's an example of how to use the third-person camera in a game loop: + +[source,cpp] +---- +void gameLoop(float deltaTime) { + // Update character position and orientation based on input + character.update(deltaTime); + + // Update camera position to follow the character + thirdPersonCamera.updatePosition( + character.getPosition(), + character.getForward(), + deltaTime + ); + + // Handle camera occlusion + thirdPersonCamera.handleOcclusion(scene); + + // Process camera orbit input (if any) + if (mouseInputDetected) { + thirdPersonCamera.orbit(mouseDeltaX, mouseDeltaY); + } + + // Get the view and projection matrices for rendering + glm::mat4 viewMatrix = thirdPersonCamera.getViewMatrix(); + glm::mat4 projMatrix = thirdPersonCamera.getProjectionMatrix(aspectRatio); + + // Use these matrices for rendering the scene + renderer.render(scene, viewMatrix, projMatrix); +} +---- + +[NOTE] +==== +For more advanced camera techniques, refer to the Advanced Camera Techniques section in the xref:../Appendix/appendix.adoc[Appendix]. +==== + +In the next section, we'll integrate our camera system with Vulkan to render 3D scenes. + +xref:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc new file mode 100644 index 00000000..cab14251 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/04_transformation_matrices.adoc @@ -0,0 +1,175 @@ +:pp: {plus}{plus} + += Camera & Transformations: Transformation Matrices + +== Transformation Matrices + +In this section, we'll dive deeper into the transformation matrices used in 3D graphics and how they're applied in our rendering pipeline. + +=== The Model-View-Projection (MVP) Pipeline + +The transformation of vertices from object space to screen space involves a series of matrix multiplications, commonly known as the MVP pipeline: + +[source,cpp] +---- +// The complete transformation pipeline +glm::mat4 MVP = projectionMatrix * viewMatrix * modelMatrix; +---- + +Let's explore each of these matrices in detail. + +=== Model Matrix + +The model matrix transforms vertices from object space to world space. It positions, rotates, and scales objects in the world. + +[source,cpp] +---- +glm::mat4 createModelMatrix( + const glm::vec3& position, + const glm::vec3& rotation, + const glm::vec3& scale +) { + // Start with identity matrix + glm::mat4 model = glm::mat4(1.0f); + + // Apply transformations in order: scale, rotate, translate + model = glm::translate(model, position); + + // Apply rotations around each axis + model = glm::rotate(model, glm::radians(rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + model = glm::rotate(model, glm::radians(rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + model = glm::rotate(model, glm::radians(rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + + // Apply scaling + model = glm::scale(model, scale); + + return model; +} +---- + +=== View Matrix + +The view matrix transforms vertices from world space to view space (camera space). It represents the position and orientation of the camera. + +[source,cpp] +---- +glm::mat4 createViewMatrix( + const glm::vec3& cameraPosition, + const glm::vec3& cameraTarget, + const glm::vec3& upVector +) { + return glm::lookAt(cameraPosition, cameraTarget, upVector); +} +---- + +The `lookAt` function creates a view matrix that positions the camera at `cameraPosition`, looking at `cameraTarget`, with `upVector` defining the up direction. + +=== Projection Matrix + +The projection matrix transforms vertices from view space to clip space. It defines how 3D coordinates are projected onto the 2D screen. + +==== Perspective Projection + +Perspective projection simulates how objects appear smaller as they get farther away, which is how our eyes naturally perceive the world. + +[source,cpp] +---- +glm::mat4 createPerspectiveMatrix( + float fovY, + float aspectRatio, + float nearPlane, + float farPlane +) { + return glm::perspective(glm::radians(fovY), aspectRatio, nearPlane, farPlane); +} +---- + +Parameters: +* `fovY`: Field of view angle in degrees (vertical) +* `aspectRatio`: Width divided by height of the viewport +* `nearPlane`: Distance to the near clipping plane +* `farPlane`: Distance to the far clipping plane + +==== Orthographic Projection + +Orthographic projection doesn't have perspective distortion, making it useful for 2D rendering or technical drawings. + +[source,cpp] +---- +glm::mat4 createOrthographicMatrix( + float left, + float right, + float bottom, + float top, + float nearPlane, + float farPlane +) { + return glm::ortho(left, right, bottom, top, nearPlane, farPlane); +} +---- + +=== Normal Matrix + +When applying non-uniform scaling to objects, normals can become incorrect if transformed with the model matrix. The normal matrix solves this issue: + +[source,cpp] +---- +glm::mat3 createNormalMatrix(const glm::mat4& modelMatrix) { + // The normal matrix is the transpose of the inverse of the upper-left 3x3 part of the model matrix + return glm::transpose(glm::inverse(glm::mat3(modelMatrix))); +} +---- + +=== Applying Transformations in Shaders + +In Vulkan, we typically pass these matrices to our shaders as uniform variables: + +[source,glsl] +---- +// Vertex shader +#version 450 + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inTexCoord; + +layout(location = 0) out vec3 fragNormal; +layout(location = 1) out vec2 fragTexCoord; + +void main() { + // Apply MVP transformation + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); + + // Transform normal using normal matrix + mat3 normalMatrix = transpose(inverse(mat3(ubo.model))); + fragNormal = normalMatrix * inNormal; + + fragTexCoord = inTexCoord; +} +---- + +=== Hierarchical Transformations + +For complex objects or scenes with parent-child relationships, we use hierarchical transformations: + +[source,cpp] +---- +// Parent transformation +glm::mat4 parentModel = createModelMatrix(parentPosition, parentRotation, parentScale); + +// Child transformation relative to parent +glm::mat4 localModel = createModelMatrix(childLocalPosition, childLocalRotation, childLocalScale); + +// Combined transformation +glm::mat4 childWorldModel = parentModel * localModel; +---- + +In the next section, we'll integrate our camera system and transformation matrices with Vulkan to render 3D scenes. + +link:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc new file mode 100644 index 00000000..2f0e8576 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/05_vulkan_integration.adoc @@ -0,0 +1,298 @@ +:pp: {plus}{plus} + += Camera & Transformations: Vulkan Integration + +== Integrating Camera with Vulkan + +=== Libraries Used in This Tutorial + +Before we dive into the integration, let's briefly introduce the key libraries we'll be using: + +* *GLFW* (Graphics Library Framework): A lightweight, multi-platform library for creating windows, contexts, and surfaces, handling input, and events. We use it for window management and input handling. [https://www.glfw.org/] + +* *GLM* (OpenGL Mathematics): A mathematics library for graphics programming that provides vector and matrix operations similar to GLSL. We use it for all our 3D math operations. [https://github.com/g-truc/glm] + +Now that we have a camera system and understand transformation matrices, let's integrate them with our Vulkan application. We'll focus on how to set up uniform buffers for our matrices and update them each frame based on camera movement. + +To keep the integration digestible, think of it in five small steps: + +* Define the UBO layout (model/view/proj) and create per-frame buffers +* Create a descriptor set layout and allocate descriptor sets for the UBO +* Write descriptor sets and persistently map the buffers for fast updates +* Update the UBO each frame from the camera (view/proj) and model transform +* Bind the descriptor set and draw using the updated matrices + +[NOTE] +==== +See link:04_transformation_matrices.adoc[Camera transformation matricies] and link:04_camera_implementation.adoc[Camera implementation] for a refresher on Matrix math. +==== + +=== Uniform Buffer Setup + +First, we need to define our uniform buffer structure: + +[source,cpp] +---- +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +---- + +Next, we'll create the uniform buffer and its descriptor set: + +[NOTE] +==== +Uniform buffers should be allocated per frame-in-flight (maxConcurrentFrames), not per swapchain image. This matches how you submit work and synchronize frames, avoids unnecessary allocations, and simplifies your logic. +==== + +[source,cpp] +---- +// Use a fixed number of frames-in-flight, not the number of swapchain images +constexpr uint32_t maxConcurrentFrames = 2; // Adjust to your renderer + +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +// Keep the mapped pointer alongside the buffer for clarity and safety +struct UboBuffer { + vk::raii::Buffer buffer{nullptr}; + vk::raii::DeviceMemory memory{nullptr}; + void* mapped = nullptr; +}; + +std::array uniformBuffers; + +void createUniformBuffers() { + vk::DeviceSize bufferSize = sizeof(UniformBufferObject); + + for (size_t i = 0; i < maxConcurrentFrames; i++) { + // Create the buffer + vk::BufferCreateInfo bufferInfo{ + .size = bufferSize, + .usage = vk::BufferUsageFlagBits::eUniformBuffer, + .sharingMode = vk::SharingMode::eExclusive + }; + + uniformBuffers[i].buffer = vk::raii::Buffer(device, bufferInfo); + + // Allocate and bind memory + vk::MemoryRequirements memRequirements = uniformBuffers[i].buffer.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo{ + .allocationSize = memRequirements.size, + .memoryTypeIndex = findMemoryType( + memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent + ) + }; + + uniformBuffers[i].memory = vk::raii::DeviceMemory(device, allocInfo); + uniformBuffers[i].buffer.bindMemory(*uniformBuffers[i].memory, 0); + + // Persistently map the buffer memory + uniformBuffers[i].mapped = uniformBuffers[i].memory.mapMemory(0, bufferSize); + } +} +---- + +=== Descriptor Set Layout + +We need to create a descriptor set layout that describes our uniform buffer: + +[source,cpp] +---- +void createDescriptorSetLayout() { + vk::DescriptorSetLayoutBinding uboLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex, + .pImmutableSamplers = nullptr + }; + + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = 1, + .pBindings = &uboLayoutBinding + }; + + descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo); +} +---- + +=== Descriptor Sets + +Now we'll create descriptor sets that point to our uniform buffers: + +[source,cpp] +---- +void createDescriptorSets() { + std::array layouts{}; + layouts.fill(*descriptorSetLayout); + + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = *descriptorPool, + .descriptorSetCount = maxConcurrentFrames, + .pSetLayouts = layouts.data() + }; + + descriptorSets = device.allocateDescriptorSets(allocInfo); + + for (size_t i = 0; i < maxConcurrentFrames; i++) { + vk::DescriptorBufferInfo bufferInfo{ + .buffer = *uniformBuffers[i].buffer, + .offset = 0, + .range = sizeof(UniformBufferObject) + }; + + vk::WriteDescriptorSet descriptorWrite{ + .dstSet = descriptorSets[i], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .pBufferInfo = &bufferInfo + }; + + device.updateDescriptorSets(1, &descriptorWrite, 0, nullptr); + } +} +---- + +=== Updating Uniform Buffers + +In our main loop, we'll update the uniform buffer with the latest camera data: + +[source,cpp] +---- +void updateUniformBuffer(uint32_t currentFrame) { + static auto startTime = std::chrono::high_resolution_clock::now(); + auto currentTime = std::chrono::high_resolution_clock::now(); + float time = std::chrono::duration(currentTime - startTime).count(); + + UniformBufferObject ubo{}; + + // Model matrix: rotate the model around the Y axis + ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f)); + + // View matrix: get from our camera + ubo.view = camera.getViewMatrix(); + + // Projection matrix: get from our camera + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + + // Vulkan's Y coordinate is inverted compared to OpenGL + ubo.proj[1][1] *= -1; + + // Copy the data to the uniform buffer for the current frame-in-flight + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); +} +---- + +=== Handling Input for Camera Movement + +We need to handle user input to control the camera: + +[source,cpp] +---- +void processInput() { + // Calculate delta time + static float lastFrame = 0.0f; + float currentFrame = glfwGetTime(); + float deltaTime = currentFrame - lastFrame; + lastFrame = currentFrame; + + // Process keyboard input for camera movement + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::LEFT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); + if (glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::UP, deltaTime); + if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) + camera.processKeyboard(CameraMovement::DOWN, deltaTime); +} +---- + +=== Mouse Callback for Camera Rotation + +We'll also need to handle mouse movement for camera rotation: + +[source,cpp] +---- +// Global variables for mouse handling +float lastX = 0.0f, lastY = 0.0f; +bool firstMouse = true; + +void mouseCallback(GLFWwindow* window, double xpos, double ypos) { + if (firstMouse) { + lastX = xpos; + lastY = ypos; + firstMouse = false; + } + + float xoffset = xpos - lastX; + float yoffset = lastY - ypos; // Reversed: y ranges bottom to top + + lastX = xpos; + lastY = ypos; + + camera.processMouseMovement(xoffset, yoffset); +} + +void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + camera.processMouseScroll(yoffset); +} +---- + +=== Setting Up Input Callbacks + +In our initialization code, we need to set up the input callbacks: + +[source,cpp] +---- +void initWindow() { + // ... existing GLFW initialization code ... + + // Set up input callbacks + glfwSetCursorPosCallback(window, mouseCallback); + glfwSetScrollCallback(window, scrollCallback); + + // Capture the cursor for camera control + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); +} +---- + +=== Main Loop Integration + +Finally, we integrate everything in our main loop: + +[source,cpp] +---- +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + processInput(); + + // Update uniform buffer with latest camera data + updateUniformBuffer(currentFrame); + + // Draw frame + drawFrame(); + } +} +---- + +With these components in place, we now have a fully functional camera system integrated with our Vulkan application. Users can navigate the 3D scene using keyboard and mouse controls, and the view will update accordingly. + +In the next section, we'll wrap up with a conclusion and discuss potential improvements to our camera system. + +link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc new file mode 100644 index 00000000..d1554b56 --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/06_conclusion.adoc @@ -0,0 +1,55 @@ +:pp: {plus}{plus} + += Camera & Transformations: Conclusion + +== Conclusion + +In this chapter, we've built a comprehensive camera system for our Vulkan application. Let's summarize what we've learned and discuss potential improvements. + +=== What We've Learned + +* *Mathematical Foundations*: We explored the essential mathematical concepts for 3D graphics, including vectors, matrices, quaternions, and coordinate systems. + +* *Camera Implementation*: We designed a flexible camera class that supports different movement modes and handles user input for navigation. + +* *Transformation Matrices*: We examined the model, view, and projection matrices that form the MVP pipeline, and how they transform vertices through different coordinate spaces. + +* *Vulkan Integration*: We integrated our camera system with Vulkan by setting up uniform buffers, descriptor sets, and input handling. + +With these components in place, we now have a solid foundation for creating interactive 3D applications with Vulkan. Our camera system allows users to navigate and explore 3D scenes from any perspective. + +=== Potential Improvements + +While our camera system is functional, there are several ways it could be enhanced: + +* *Camera Modes*: Implement different camera modes (first-person, third-person, orbit) that can be switched at runtime. + +* *Smooth Transitions*: Add interpolation between camera positions and orientations for smoother transitions. + +* *Collision Detection*: Implement collision detection to prevent the camera from passing through objects or walls. + +* *Camera Paths*: Create a system for defining and following predefined camera paths for cinematic sequences. + +* *Camera Effects*: Add support for camera effects like depth of field, motion blur, or screen-space reflections. + +* *Performance Optimization*: Optimize the camera system for performance, especially for mobile or VR applications. + +=== Next Steps + +As you continue building your Vulkan engine, consider how the camera system integrates with other components: + +* *Scene Graph*: How does the camera fit into your scene graph hierarchy? + +* *Rendering Pipeline*: How can you optimize rendering based on the camera's position and orientation? + +* *User Interface*: How will users interact with the camera in your application? + +By addressing these questions, you can create a more cohesive and user-friendly 3D application. + +=== Final Thoughts + +A well-designed camera system is essential for any 3D application. It serves as the user's window into your virtual world and significantly impacts the user experience. By understanding the mathematical foundations and implementing a flexible camera system, you've taken a major step toward creating immersive 3D applications with Vulkan. + +Remember that the code provided in this chapter is a starting point. Feel free to modify and extend it to suit your specific needs and application requirements. + +xref:../Engine_Architecture/conclusion.adoc[Previous: Engine Architecture] | xref:../Lighting_Materials/01_introduction.adoc[Next: Lighting & Materials] diff --git a/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc b/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc new file mode 100644 index 00000000..fbc3369f --- /dev/null +++ b/en/Building_a_Simple_Engine/Camera_Transformations/index.adoc @@ -0,0 +1,14 @@ +:pp: {plus}{plus} + += Camera & Transformations + +This chapter covers the implementation of a 3D camera system and the mathematical foundations of 3D transformations in Vulkan. + +== Contents + +* link:01_introduction.adoc[Introduction] +* link:02_math_foundations.adoc[Mathematical Foundations] +* link:03_camera_implementation.adoc[Camera Implementation] +* link:04_transformation_matrices.adoc[Transformation Matrices] +* link:05_vulkan_integration.adoc[Vulkan Integration] +* link:06_conclusion.adoc[Conclusion] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc new file mode 100644 index 00000000..2de95723 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/01_introduction.adoc @@ -0,0 +1,62 @@ +:pp: {plus}{plus} + += Engine Architecture: Introduction + +== Introduction + +Welcome to the "Engine Architecture" chapter of our "Building a Simple Game +Engine" series! In this chapter, we'll explore the fundamental architectural +patterns and design principles that form the backbone of a modern Vulkan +rendering engine. + +While this series focuses primarily on building a rendering engine with Vulkan, +the architectural concepts we'll discuss are applicable to both rendering engines +and full game engines. We'll clarify which patterns are particularly well-suited +for rendering-focused systems versus more general game engine development. + +We'll start by taking a step back and considering the overall structure of our engine. A +well-designed architecture is crucial for creating a flexible, maintainable, and extensible rendering system. + +=== What You'll Learn + +This chapter will take you through the foundational concepts that underpin effective engine design. We'll begin by exploring architectural patterns—the proven design approaches that game and rendering engines rely on to manage complexity and enable extensibility. Understanding these patterns helps you choose the right structural approach for different engine subsystems. + +From there, we'll dive into component systems, which provide the flexibility to build modular, reusable code. You'll see how component-based architecture allows different parts of your engine to work together while remaining loosely coupled, making your codebase easier to maintain and extend. + +Resource management forms another crucial pillar of engine architecture. We'll examine strategies for efficiently handling textures, models, shaders, and other assets, ensuring your engine can scale from simple scenes to complex, asset-heavy applications without performance bottlenecks. + +The rendering pipeline design we'll cover shows you how to create a flexible system that can accommodate various rendering techniques and effects. This foundation will serve you well as you add more advanced rendering features in later chapters. + +Finally, we'll implement event systems that enable clean communication between different engine components. This event-driven approach reduces tight coupling and makes your engine more maintainable as it grows in complexity. + +=== Prerequisites + +This chapter builds directly on the foundation established in the main Vulkan tutorial series, so completing that series is essential. The architectural concepts we'll discuss assume you're comfortable with Vulkan's core rendering concepts and have hands-on experience implementing them. + +Beyond Vulkan knowledge, you'll benefit from familiarity with object-oriented programming principles, as modern engine architecture relies heavily on encapsulation, inheritance, and polymorphism to manage complexity. Experience with common design patterns like Observer, Factory, and Singleton will help you recognize when and why we apply these patterns in our engine design. + +Modern C++ features play a crucial role in our implementation approach. Smart pointers help us manage resource lifetimes safely, templates enable flexible, reusable components, and other C++11/14/17 features allow us to write more expressive and maintainable code. If you're not comfortable with these concepts, consider reviewing them before proceeding. + +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] + +=== Why Architecture Matters + +The difference between a hastily assembled renderer and a well-architected engine becomes apparent as soon as you need to make changes or add features. Good architecture creates a foundation that supports your development process rather than fighting against it. + +Maintainability emerges from clean separation of concerns—when each component has a clear, focused responsibility, you can update or fix individual pieces without worrying about cascading effects throughout the system. This becomes invaluable when debugging graphics issues or implementing new rendering techniques. + +Extensibility flows naturally from modular design. When your architecture provides clear extension points and interfaces, adding new features becomes a matter of implementing new components rather than rewriting existing systems. This allows your engine to evolve with your project's needs. + +Reusability multiplies your development effort. Well-encapsulated components can move between projects or serve different purposes within the same project. A thoughtfully designed material system, for example, might work equally well for both game objects and UI elements. + +Performance opportunities often emerge from architectural decisions made early in development. Good architecture enables optimizations like multithreading (by avoiding tight coupling between systems), batching (through predictable interfaces), and caching (via clear data flow patterns). While premature optimization is dangerous, premature architecture decisions can make later optimization impossible. + +Let's begin our exploration of engine architecture with an overview of common architectural patterns used in modern rendering engines. + +link:02_architectural_patterns.adoc[Next: Architectural Patterns] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc new file mode 100644 index 00000000..e09bce33 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/02_architectural_patterns.adoc @@ -0,0 +1,171 @@ +:pp: {plus}{plus} + += Engine Architecture: Architectural Patterns + +== Architectural Patterns + +In this section, we'll provide a quick overview of common architectural patterns used in modern rendering and game engines, with a focus on Component-Based Architecture which forms the foundation of our Vulkan rendering engine. + +Before diving into specific patterns, it's important to clarify that while we're building a Vulkan-based rendering engine in this tutorial, many of the architectural patterns we'll discuss are commonly used in both rendering engines and full game engines. A rendering engine focuses primarily on graphics rendering capabilities, while a full game engine typically includes additional systems like physics, audio, AI, and gameplay logic. + +=== Overview of Common Architectural Patterns + +Here's a brief introduction to the most common architectural patterns used in game and rendering engines: + +==== link:https://games-1312234642.cos.ap-guangzhou.myqcloud.com/course/GAMES104/GAMES104_Lecture02.pdf[Layered Architecture] + +Layered architecture divides the system into distinct layers, each with a specific responsibility. Typical layers include platform abstraction, resource management, rendering, scene management, and application layers. + +image::../../../images/layered_architecture_diagram.png[Layered Architecture Diagram, width=400, alt="Layered Architecture Diagram showing different layers of a rendering engine"] + +*Key Benefits:* +* Clear separation of concerns +* Easier to understand and maintain +* Can replace or modify individual layers without affecting others + +For detailed information and implementation examples, see the xref:../Appendix/appendix.adoc#layered-architecture[Appendix: Layered Architecture]. + +==== link:https://www.youtube.com/watch?v=rX0ItVEVjHc[Data-Oriented Design] + +Data-Oriented Design (DOD) focuses on organizing data for efficient processing rather than organizing code around objects. It emphasizes cache-friendly memory layouts and bulk processing of data. + +image::../../../images/data_oriented_design_diagram.svg[Data-Oriented Design Diagram, width=400] + +*Key Benefits:* +* Better cache utilization +* More efficient memory usage +* Easier to parallelize + +For detailed information and implementation examples, see the xref:../Appendix/appendix.adoc#data-oriented-design[Appendix: Data-Oriented Design]. + +==== link:https://gameprogrammingpatterns.com/service-locator.html[Service Locator Pattern] + +The Service Locator pattern provides a global point of access to services without coupling consumers to concrete implementations. + +image::../../../images/service_locator_pattern_diagram.svg[Service Locator Pattern Diagram, width=400] + +*Key Benefits:* +* Decouples service consumers from service providers +* Allows for easy service replacement +* Facilitates testing with mock services + +For detailed information and implementation examples, see the xref:../Appendix/appendix.adoc#service-locator-pattern[Appendix: Service Locator Pattern]. + +=== link:https://gameprogrammingpatterns.com/component.html[Component-Based Architecture] + +Component-based architecture is widely used in modern game engines and forms the foundation of our Vulkan rendering engine. It promotes composition over inheritance and allows for more flexible entity design. + +image::../../../images/component_based_architecture_diagram.png[Component-Based Architecture Diagram, width=600, alt="Component-Based Architecture Diagram showing entities, components, and systems"] + +[NOTE] +==== +*Diagram Legend:* + +* *Boxes*: Blue boxes represent Entities, orange boxes represent Components, and green boxes represent Systems +* *Line Types*: +** Dashed lines show ownership/containment (Entities contain Components) +** Solid lines show processing relationships (Systems process specific Components) +* *Text*: All text elements use dark colors for visibility in both light and dark modes +* *Directional Flow*: Arrows indicate the direction of relationships between elements +==== + +==== Key Concepts + +1. *Entities* - Basic containers that represent objects in the game world. +2. *Components* - Modular pieces of functionality that can be attached to entities. +3. *Systems* - Process entities with specific components to implement game logic. + +==== Benefits of Component-Based Architecture + +* Highly modular and flexible +* Avoids deep inheritance hierarchies +* Enables data-oriented design +* Facilitates parallel processing + +==== Implementation Example + +[source,cpp] +---- +// Component base class +class Component { +public: + virtual ~Component() = default; + virtual void Update(float deltaTime) {} +}; + +// Specific component types +class TransformComponent : public Component { +private: + glm::vec3 position; + glm::quat rotation; + glm::vec3 scale; + +public: + // Methods to manipulate transform +}; + +class MeshComponent : public Component { +private: + Mesh* mesh; + Material* material; + +public: + // Methods to render the mesh +}; + +// Entity class +class Entity { +private: + std::vector> components; + +public: + template + T* AddComponent(Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Component"); + auto component = std::make_unique(std::forward(args)...); + T* componentPtr = component.get(); + components.push_back(std::move(component)); + return componentPtr; + } + + template + T* GetComponent() { + for (auto& component : components) { + if (T* result = dynamic_cast(component.get())) { + return result; + } + } + return nullptr; + } + + void Update(float deltaTime) { + for (auto& component : components) { + component->Update(deltaTime); + } + } +}; +---- + +=== Why We're Focusing on Component Systems + +For our Vulkan rendering engine, we've chosen to focus on component-based architecture for several key reasons: + +1. *Flexibility for Graphics Features*: Component systems allow us to easily add, remove, or swap rendering features (like different shading models, post-processing effects, or lighting techniques) without major refactoring. + +2. *Separation of Rendering Concerns*: Components naturally separate different aspects of rendering (geometry, materials, lighting, cameras) into manageable, reusable pieces. + +3. *Industry Standard*: Most modern rendering engines and graphics frameworks use component-based approaches because they provide the right balance of flexibility, maintainability, and performance. + +4. *Extensibility*: As graphics technology evolves rapidly, component systems make it easier to incorporate new Vulkan features or rendering techniques. + +5. *Compatibility with Data-Oriented Optimizations*: While we're using a component-based approach, we can still apply data-oriented design principles within our components for performance-critical rendering paths. + +While other architectural patterns have their merits, component-based architecture provides the best foundation for a modern, flexible rendering engine. That said, we'll incorporate aspects of other patterns where appropriate - using layered architecture for our overall engine structure, data-oriented design for performance-critical systems, and service locators for cross-cutting concerns. + +=== Conclusion + +We've provided a brief overview of common architectural patterns, with a focus on Component-Based Architecture which we'll use throughout this tutorial. For more detailed information about other architectural patterns, including implementation examples and comparative analysis, see the xref:../Appendix/appendix.adoc[Appendix: Detailed Architectural Patterns]. + +In the next section, we'll dive deeper into component systems and how to implement them effectively in your engine. + +xref:01_introduction.adoc[Previous: Introduction] | xref:03_component_systems.adoc[Next: Component Systems] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc new file mode 100644 index 00000000..b77b20f4 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/03_component_systems.adoc @@ -0,0 +1,552 @@ +:pp: {plus}{plus} + += Engine Architecture: Component Systems + +== Component Systems + +In the previous section, we introduced several architectural patterns and explained why we're focusing on component-based architecture for our Vulkan rendering engine. As we established, component systems provide the ideal balance of flexibility, modularity, and performance for modern rendering engines. Now, let's dive deeper into how to implement effective component systems in your rendering engine. + +=== The Problem with Deep Inheritance + +Traditional game object systems often rely on deep inheritance hierarchies: + +[source,cpp] +---- +class GameObject { /* ... */ }; +class PhysicalObject : public GameObject { /* ... */ }; +class Character : public PhysicalObject { /* ... */ }; +class Player : public Character { /* ... */ }; +class Enemy : public Character { /* ... */ }; +class FlyingEnemy : public Enemy { /* ... */ }; +// And so on... +---- + +This approach has several drawbacks: + +1. *Rigidity* - Adding new combinations of behaviors requires creating new classes. +2. *Code Duplication* - Similar functionality may be duplicated across different branches of the hierarchy. +3. *Bloated Classes* - Base classes tend to accumulate functionality over time. +4. *Difficult Refactoring* - Changes to base classes can have far-reaching consequences. + +=== Component-Based Design Principles + +Component-based design addresses these issues by favoring composition over inheritance: + +1. *Single Responsibility* - Each component should have a single, well-defined responsibility. +2. *Encapsulation* - Components should encapsulate their internal state and behavior. +3. *Loose Coupling* - Components should minimize dependencies on other components. +4. *Reusability* - Components should be designed for reuse across different entity types. + +=== Basic Component System Implementation + +Let's build a more complete component system based on the example from the previous section: + +[source,cpp] +---- +// Forward declarations +class Entity; + +// Base component class +class Component { +protected: + Entity* owner = nullptr; + +public: + virtual ~Component() = default; + + virtual void Initialize() {} + virtual void Update(float deltaTime) {} + virtual void Render() {} + + void SetOwner(Entity* entity) { owner = entity; } + Entity* GetOwner() const { return owner; } +}; + +// Entity class +class Entity { +private: + std::string name; + bool active = true; + std::vector> components; + +public: + explicit Entity(const std::string& entityName) : name(entityName) {} + + const std::string& GetName() const { return name; } + bool IsActive() const { return active; } + void SetActive(bool isActive) { active = isActive; } + + void Initialize() { + for (auto& component : components) { + component->Initialize(); + } + } + + void Update(float deltaTime) { + if (!active) return; + + for (auto& component : components) { + component->Update(deltaTime); + } + } + + void Render() { + if (!active) return; + + for (auto& component : components) { + component->Render(); + } + } + + template + T* AddComponent(Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Component"); + + // Check if component of this type already exists + for (auto& component : components) { + if (dynamic_cast(component.get())) { + return dynamic_cast(component.get()); + } + } + + // Create new component + auto component = std::make_unique(std::forward(args)...); + T* componentPtr = component.get(); + componentPtr->SetOwner(this); + components.push_back(std::move(component)); + return componentPtr; + } + + template + T* GetComponent() { + for (auto& component : components) { + if (T* result = dynamic_cast(component.get())) { + return result; + } + } + return nullptr; + } + + template + bool RemoveComponent() { + for (auto it = components.begin(); it != components.end(); ++it) { + if (dynamic_cast(it->get())) { + components.erase(it); + return true; + } + } + return false; + } +}; +---- + +=== Common Component Types + +Let's implement some common component types that you might use in a rendering engine: + +[source,cpp] +---- +// Transform component +// Handles the position, rotation, and scale of an entity in 3D space +// AffineTransform or "Pose" matrix. +class TransformComponent : public Component { +private: + 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); + + // Cached transformation matrix + mutable glm::mat4 transformMatrix = glm::mat4(1.0f); + mutable bool transformDirty = true; + +public: + void SetPosition(const glm::vec3& pos) { + position = pos; + transformDirty = true; + } + + void SetRotation(const glm::quat& rot) { + rotation = rot; + transformDirty = true; + } + + void SetScale(const glm::vec3& s) { + scale = s; + transformDirty = true; + } + + const glm::vec3& GetPosition() const { return position; } + const glm::quat& GetRotation() const { return rotation; } + const glm::vec3& GetScale() const { return scale; } + + glm::mat4 GetTransformMatrix() const { + if (transformDirty) { + // Calculate transformation matrix + glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), position); + glm::mat4 rotationMatrix = glm::mat4_cast(rotation); + glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), scale); + + transformMatrix = translationMatrix * rotationMatrix * scaleMatrix; + transformDirty = false; + } + return transformMatrix; + } +}; + +// Mesh component +// Manages the visual representation of an entity by handling its 3D mesh and material +class MeshComponent : public Component { +private: + Mesh* mesh = nullptr; + Material* material = nullptr; + +public: + MeshComponent(Mesh* m, Material* mat) : mesh(m), material(mat) {} + + void SetMesh(Mesh* m) { mesh = m; } + void SetMaterial(Material* mat) { material = mat; } + + Mesh* GetMesh() const { return mesh; } + Material* GetMaterial() const { return material; } + + void Render() override { + if (!mesh || !material) return; + + // Get transform component + auto transform = GetOwner()->GetComponent(); + if (!transform) return; + + // Render mesh with material and transform + material->Bind(); + material->SetUniform("modelMatrix", transform->GetTransformMatrix()); + mesh->Render(); + } +}; + +// Camera component +// Defines a viewpoint for rendering the scene by managing view and projection matrices +class CameraComponent : public Component { +private: + float fieldOfView = 45.0f; + float aspectRatio = 16.0f / 9.0f; + float nearPlane = 0.1f; + float farPlane = 1000.0f; + + glm::mat4 viewMatrix = glm::mat4(1.0f); + glm::mat4 projectionMatrix = glm::mat4(1.0f); + bool projectionDirty = true; + +public: + void SetPerspective(float fov, float aspect, float near, float far) { + fieldOfView = fov; + aspectRatio = aspect; + nearPlane = near; + farPlane = far; + projectionDirty = true; + } + + glm::mat4 GetViewMatrix() const { + // Get transform component + auto transform = GetOwner()->GetComponent(); + if (transform) { + // Calculate view matrix from transform + glm::vec3 position = transform->GetPosition(); + glm::quat rotation = transform->GetRotation(); + + // Forward vector (local -Z) + glm::vec3 forward = rotation * glm::vec3(0.0f, 0.0f, -1.0f); + // Up vector (local +Y) + glm::vec3 up = rotation * glm::vec3(0.0f, 1.0f, 0.0f); + + return glm::lookAt(position, position + forward, up); + } + return glm::mat4(1.0f); + } + + glm::mat4 GetProjectionMatrix() const { + if (projectionDirty) { + projectionMatrix = glm::perspective( + glm::radians(fieldOfView), + aspectRatio, + nearPlane, + farPlane + ); + projectionDirty = false; + } + return projectionMatrix; + } +}; +---- + +=== Component Communication + +Components often need to communicate with each other. There are several approaches to component communication: + +==== Direct References + +The simplest approach is to use direct references: + +[source,cpp] +---- +void MeshComponent::Update(float deltaTime) { + auto transform = GetOwner()->GetComponent(); + if (transform) { + // Use transform data + } +} +---- + +This approach is straightforward but creates tight coupling between +components. Tight coupling makes it challenging or impossible to create +unit tests and properly test the engine, so this approach should be avoided +in production code. + +==== Event System + +A more flexible approach is to use an event system: + +[source,cpp] +---- +// Event base class +class Event { +public: + virtual ~Event() = default; +}; + +// Specific event types +class CollisionEvent : public Event { +private: + Entity* entity1; + Entity* entity2; + +public: + CollisionEvent(Entity* e1, Entity* e2) : entity1(e1), entity2(e2) {} + + Entity* GetEntity1() const { return entity1; } + Entity* GetEntity2() const { return entity2; } +}; + +// Event listener interface +class EventListener { +public: + virtual ~EventListener() = default; + virtual void OnEvent(const Event& event) = 0; +}; + +// Event system +class EventSystem { +private: + std::vector listeners; + +public: + void AddListener(EventListener* listener) { + listeners.push_back(listener); + } + + void RemoveListener(EventListener* listener) { + auto it = std::find(listeners.begin(), listeners.end(), listener); + if (it != listeners.end()) { + listeners.erase(it); + } + } + + void DispatchEvent(const Event& event) { + for (auto listener : listeners) { + listener->OnEvent(event); + } + } +}; + +// Component that listens for events +// Handles physics-related behavior and responds to collision events through the event system +class PhysicsComponent : public Component, public EventListener { +public: + void Initialize() override { + // Register as event listener + GetEventSystem().AddListener(this); + } + + ~PhysicsComponent() override { + // Unregister as event listener + GetEventSystem().RemoveListener(this); + } + + void OnEvent(const Event& event) override { + if (auto collisionEvent = dynamic_cast(&event)) { + // Handle collision event + } + } + +private: + EventSystem& GetEventSystem() { + // Get event system from somewhere (e.g., service locator) + static EventSystem eventSystem; + return eventSystem; + } +}; +---- + +This approach decouples components but adds complexity. Crucially, a +decoupled component is a component that can be tested independently of any +other component. + +=== Component Lifecycle Management + +Managing the lifecycle of components is crucial for a robust component system: + +[source,cpp] +---- +class Component { +public: + enum class State { + Uninitialized, + Initializing, + Active, + Destroying, + Destroyed + }; + +private: + State state = State::Uninitialized; + Entity* owner = nullptr; + +public: + virtual ~Component() { + if (state != State::Destroyed) { + OnDestroy(); + state = State::Destroyed; + } + } + + void Initialize() { + if (state == State::Uninitialized) { + state = State::Initializing; + OnInitialize(); + state = State::Active; + } + } + + void Destroy() { + if (state == State::Active) { + state = State::Destroying; + OnDestroy(); + state = State::Destroyed; + } + } + + bool IsActive() const { return state == State::Active; } + + void SetOwner(Entity* entity) { owner = entity; } + Entity* GetOwner() const { return owner; } + +protected: + virtual void OnInitialize() {} + virtual void OnDestroy() {} + virtual void Update(float deltaTime) {} + virtual void Render() {} + + friend class Entity; // Allow Entity to call protected methods +}; +---- + +=== Optimizing Component Access + +The `GetComponent()` method shown earlier uses dynamic_cast, which can be slow. Here's an optimized approach using component type IDs: + +[source,cpp] +---- +// Component type ID system +class ComponentTypeIDSystem { +private: + static size_t nextTypeID; + +public: + template + static size_t GetTypeID() { + static size_t typeID = nextTypeID++; + return typeID; + } +}; + +size_t ComponentTypeIDSystem::nextTypeID = 0; + +// Component base class with type ID +class Component { +public: + virtual ~Component() = default; + + template + static size_t GetTypeID() { + return ComponentTypeIDSystem::GetTypeID(); + } +}; + +// Entity with optimized component access +class Entity { +private: + std::vector> components; + std::unordered_map componentMap; + +public: + template + T* AddComponent(Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from Component"); + + size_t typeID = Component::GetTypeID(); + + // Check if component of this type already exists + auto it = componentMap.find(typeID); + if (it != componentMap.end()) { + return static_cast(it->second); + } + + // Create new component + auto component = std::make_unique(std::forward(args)...); + T* componentPtr = component.get(); + componentMap[typeID] = componentPtr; + components.push_back(std::move(component)); + return componentPtr; + } + + template + T* GetComponent() { + size_t typeID = Component::GetTypeID(); + auto it = componentMap.find(typeID); + if (it != componentMap.end()) { + return static_cast(it->second); + } + return nullptr; + } + + template + bool RemoveComponent() { + size_t typeID = Component::GetTypeID(); + auto it = componentMap.find(typeID); + if (it != componentMap.end()) { + Component* componentPtr = it->second; + componentMap.erase(it); + + for (auto compIt = components.begin(); compIt != components.end(); ++compIt) { + if (compIt->get() == componentPtr) { + components.erase(compIt); + return true; + } + } + } + return false; + } +}; +---- + +=== Conclusion + +Component systems provide a flexible and modular approach to building game objects in your engine. By following the principles outlined in this section, you can create a robust component system that: + +1. Promotes code reuse through composition +2. Reduces coupling between different parts of your engine +3. Allows for flexible entity creation without deep inheritance hierarchies +4. Can be optimized for performance + +In the next section, we'll explore resource management systems, which are crucial for efficiently handling assets in your engine. + +link:02_architectural_patterns.adoc[Previous: Architectural Patterns] | link:04_resource_management.adoc[Next: Resource Management] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc new file mode 100644 index 00000000..735a2c0b --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/04_resource_management.adoc @@ -0,0 +1,900 @@ +:pp: {plus}{plus} + += Engine Architecture: Resource Management + +== Resource Management + +Efficient resource management is a critical aspect of any rendering engine. In this section, we'll explore strategies for managing various types of resources, such as textures, meshes, shaders, and materials. + +=== Resource Management Challenges + +When designing a resource management system, you'll need to address several challenges: + +1. *Loading and Unloading* - Resources need to be loaded from disk and unloaded when no longer needed. +2. *Caching* - Frequently used resources should be cached to avoid redundant loading. +3. *Reference Counting* - Track how many objects are using a resource to know when it can be safely unloaded. +4. *Hot Reloading* - Allow resources to be updated while the application is running (useful during development). +5. *Streaming* - Load resources asynchronously to avoid blocking the main thread. It's good to realize that "streaming" here is meant in terms of sending data from one location to another in chunks. It's the same type of algorithm that might be familiar in networking or internet downloading, however, it only differs in the sense that it relates to transferring data between the system memory and the GPU memory. +6. *Memory Management* - Efficiently allocate and deallocate memory for resources. + +=== Resource Handles + +Instead of directly exposing resource pointers, it's often better to use resource handles: + +[source,cpp] +---- +// Resource handle +template +class ResourceHandle { +private: + std::string resourceId; + ResourceManager* resourceManager; + +public: + ResourceHandle() : resourceManager(nullptr) {} + + ResourceHandle(const std::string& id, ResourceManager* manager) + : resourceId(id), resourceManager(manager) {} + + T* Get() const { + if (!resourceManager) return nullptr; + return resourceManager->GetResource(resourceId); + } + + bool IsValid() const { + return resourceManager && resourceManager->HasResource(resourceId); + } + + const std::string& GetId() const { + return resourceId; + } + + // Convenience operators + T* operator->() const { + return Get(); + } + + T& operator*() const { + return *Get(); + } + + operator bool() const { + return IsValid(); + } +}; +---- + +Using handles instead of direct pointers provides several benefits: + +1. *Indirection* - The resource manager can move resources in memory without invalidating references. +2. *Validation* - Handles can be checked for validity before use. +3. *Automatic Resource Management* - The resource manager can track which resources are in use. + +=== Basic Resource Manager + +Let's implement a basic resource manager that can handle different types of resources. This implementation involves several key steps that work together to provide efficient resource management for a rendering engine. + +=== Resource Manager: Base Resource Architecture and State Management + +First, we establish the fundamental infrastructure for resource management, defining how resources track their identity and loading state within the system. + +[source,cpp] +---- +// Resource base class +class Resource { +private: + std::string resourceId; // Unique identifier for this resource within the system + bool loaded = false; // Loading state flag for resource lifecycle management + +public: + explicit Resource(const std::string& id) : resourceId(id) {} + virtual ~Resource() = default; + + // Core resource identity and state access methods + const std::string& GetId() const { return resourceId; } + bool IsLoaded() const { return loaded; } + + // Virtual interface for resource-specific loading and unloading behavior + virtual bool Load() { + loaded = true; + return true; + } + + virtual void Unload() { + loaded = false; + } +}; +---- + +The Resource base class provides the foundational contract that all resource types must fulfill. The resource ID serves as a unique identifier that allows the resource manager to locate and reference specific resources without ambiguity. This string-based approach enables human-readable resource names like "main_character_texture" or "level_1_audio" while maintaining the flexibility to use file paths or other naming schemes. + +The loading state management through the boolean flag provides essential lifecycle tracking. This simple approach allows systems to quickly determine whether a resource is ready for use without expensive validation checks. The virtual loading interface enables polymorphic behavior where different resource types can implement their own specialized loading logic while presenting a consistent interface to the management system. + +=== Resource Manager: Storage Architecture and Type Safety + +Next, we implement the core storage system that organizes resources by type while maintaining type safety and efficient access patterns. + +[source,cpp] +---- +// Resource manager +class ResourceManager { +private: + // Two-level storage system: organize by type first, then by unique identifier + // This approach enables type-safe resource access while maintaining efficient lookup + std::unordered_map>> resources; + + // Reference counting system for automatic resource lifecycle management + // Maps resource IDs to their current usage count for garbage collection + std::unordered_map refCounts; +---- + +The storage architecture uses a sophisticated two-level mapping system that solves several critical problems in resource management. The outer map keyed by `std::type_index` ensures complete type separation, preventing name collisions between different resource types. For example, you could have both a texture named "stone" and a sound effect named "stone" without conflicts, as they're stored in separate type-specific containers. + +The inner maps provide O(1) average-case lookup performance for individual resources, which is crucial when the rendering system needs to access hundreds or thousands of resources per frame. The use of `std::shared_ptr` provides automatic memory management and enables safe sharing of resources between different systems without manual lifetime management. + +The reference counting system operates independently of the shared_ptr reference counting to provide application-level lifecycle control. This separation allows the resource manager to implement custom policies for resource retention and cleanup that go beyond simple memory management, such as keeping frequently used resources loaded even when not immediately referenced. + +=== Resource Manager: Resource Loading and Caching Logic + +Then, we implement the intelligent resource loading system that handles caching, reference counting, and error recovery for efficient resource management. + +[source,cpp] +---- +public: + template + ResourceHandle Load(const std::string& resourceId) { + static_assert(std::is_base_of::value, "T must derive from Resource"); + + // Step 3a: Check existing resource cache to avoid redundant loading + auto& typeResources = resources[std::type_index(typeid(T))]; + auto it = typeResources.find(resourceId); + + if (it != typeResources.end()) { + // Resource exists in cache - increment reference count and return handle + refCounts[resourceId]++; + return ResourceHandle(resourceId, this); + } + + // Step 3b: Create new resource instance and attempt loading + auto resource = std::make_shared(resourceId); + if (!resource->Load()) { + // Loading failed - return invalid handle rather than corrupting cache + return ResourceHandle(); + } + + // Step 3c: Cache successful resource and initialize reference tracking + typeResources[resourceId] = resource; + refCounts[resourceId] = 1; + + return ResourceHandle(resourceId, this); + } +---- + +The loading logic implements a sophisticated caching strategy that balances performance with memory efficiency. The cache-first approach prevents redundant I/O operations and resource processing, which can be expensive for large textures, complex meshes, or compiled shaders. This strategy is particularly important in rendering engines where the same resources may be referenced by multiple objects or systems. + +The template-based design with compile-time type checking ensures type safety while maintaining the flexibility to work with any resource type that derives from the base Resource class. The static assertion provides clear error messages during development, preventing runtime type errors that could be difficult to debug in complex rendering scenarios. + +Error handling follows the principle of graceful degradation, where loading failures return invalid handles rather than throwing exceptions or corrupting the resource cache. This approach allows rendering systems to continue operating with fallback resources or alternative rendering paths when specific assets are unavailable or corrupted. + +=== Resource Manager: Resource Access and Validation Interface + +After that, we provide the interface for safely accessing cached resources with proper validation and type checking throughout the resource lifecycle. + +[source,cpp] +---- + template + T* GetResource(const std::string& resourceId) { + // Access type-specific resource container using compile-time type information + auto& typeResources = resources[std::type_index(typeid(T))]; + auto it = typeResources.find(resourceId); + + if (it != typeResources.end()) { + // Resource found - perform safe downcast and return typed pointer + return static_cast(it->second.get()); + } + + // Resource not found - return null for safe handling by caller + return nullptr; + } + + template + bool HasResource(const std::string& resourceId) { + // Efficient existence check without resource access overhead + auto& typeResources = resources[std::type_index(typeid(T))]; + return typeResources.find(resourceId) != typeResources.end(); + } +---- + +The resource access interface prioritizes safety and performance in equal measure. The template-based approach ensures that clients always receive correctly typed resource pointers, eliminating the need for manual casting and reducing the potential for type-related runtime errors. The static_cast is safe because the type_index-based storage guarantees that only objects of type T are stored in each type-specific container. + +The existence check provides an efficient way to validate resource availability without the overhead of full resource access. This capability is valuable for conditional rendering logic, where systems can choose alternative rendering paths based on resource availability without triggering expensive cache misses or I/O operations. + +=== Resource Manager: Reference Counting and Automatic Cleanup + +Finally, we implement intelligent resource lifecycle management through reference counting and automatic cleanup to prevent memory leaks and optimize resource utilization. + +[source,cpp] +---- + void Release(const std::string& resourceId) { + // Locate reference count entry for this resource + auto it = refCounts.find(resourceId); + if (it != refCounts.end()) { + it->second--; + + // Check if resource has no remaining references + if (it->second <= 0) { + // Step 5a: Locate and unload the unreferenced resource across all type containers + for (auto& [type, typeResources] : resources) { + auto resourceIt = typeResources.find(resourceId); + if (resourceIt != typeResources.end()) { + resourceIt->second->Unload(); // Allow resource to clean up its data + typeResources.erase(resourceIt); // Remove from cache + break; + } + } + + // Step 5b: Clean up reference counting entry + refCounts.erase(it); + } + } + } + + void UnloadAll() { + // Emergency cleanup method for system shutdown or major state changes + for (auto& [type, typeResources] : resources) { + for (auto& [id, resource] : typeResources) { + resource->Unload(); // Ensure all resources clean up properly + } + typeResources.clear(); // Clear type-specific containers + } + refCounts.clear(); // Reset all reference counts + } +}; +---- + +The reference counting system provides automatic garbage collection for resources that are no longer actively used. This approach prevents memory leaks while avoiding the overhead of constantly monitoring resource usage across the entire application. The decrement-and-check pattern ensures that resources are unloaded immediately when they become unused, helping to keep memory usage optimal. + +The cleanup process is designed to be thorough and safe, ensuring that resources have the opportunity to properly release their internal data (GPU memory, file handles, etc.) before being removed from the cache. This two-phase cleanup approach prevents resource leaks and maintains system stability even under error conditions. + +The global unload functionality provides a safety valve for major state transitions like level changes or application shutdown, where you want to ensure all resources are properly cleaned up regardless of their reference counts. This capability is essential for preventing resource leaks that could accumulate over long application runs. + +=== Implementing Specific Resource Types + +Now let's implement some specific resource types that demonstrate how different asset types can be integrated into our resource management system. These implementations showcase the flexibility of the base Resource interface while addressing the unique requirements of different content types. + +=== Texture Resource Implementation + +The Texture resource represents one of the most complex resource types in a rendering engine, requiring careful management of GPU memory, format conversion, and sampling parameters. Let's break this implementation into logical phases that demonstrate both the technical challenges and design solutions. + +=== Texture Resource: Resource Structure and Vulkan State Management + +First, we establish the fundamental data structures required for Vulkan texture management, including GPU resources and metadata needed for proper texture usage. + +[source,cpp] +---- +// Texture resource +class Texture : public Resource { +private: + // Core Vulkan GPU resources for texture representation + vk::Image image; // GPU image object containing pixel data + vk::DeviceMemory memory; // GPU memory allocation backing the image + vk::ImageView imageView; // Shader-accessible view into the image + vk::Sampler sampler; // Sampling configuration (filtering, wrapping, etc.) + + // Texture metadata for validation and debugging + int width = 0; // Image width in pixels + int height = 0; // Image height in pixels + int channels = 0; // Number of color channels (RGB=3, RGBA=4, etc.) + +public: + explicit Texture(const std::string& id) : Resource(id) {} + + ~Texture() override { + Unload(); // Ensure proper cleanup when object is destroyed + } +---- + +The Vulkan texture pipeline requires four distinct GPU objects that work together to provide complete texture functionality. The `vk::Image` represents the actual pixel data storage on the GPU, while `vk::DeviceMemory` provides the backing memory allocation. The separation between image and memory allows for advanced memory management techniques like suballocation and memory pooling. + +The `vk::ImageView` serves as the interface between shaders and the image data, defining how shaders interpret the pixel format, mipmap levels, and array layers. The `vk::Sampler` encapsulates filtering and addressing modes that control how the GPU interpolates between pixels and handles texture coordinates outside the [0,1] range. This separation of concerns allows the same image to be used with different sampling configurations simultaneously. + +=== Texture Resource: Loading Pipeline and Data Acquisition + +Next, we implement the texture loading pipeline that transforms disk-based image files into GPU-ready resources through careful error handling and format conversion. + +[source,cpp] +---- + bool Load() override { + // Step 2a: Construct file path using resource ID and expected format + std::string filePath = "textures/" + GetId() + ".ktx"; + + // Step 2b: Load raw image data from disk with format detection + unsigned char* data = LoadImageData(filePath, &width, &height, &channels); + if (!data) { + return false; // Failed to load - return failure without partial state + } + + // Step 2c: Transform raw pixel data into Vulkan GPU resources + CreateVulkanImage(data, width, height, channels); + + // Step 2d: Clean up temporary CPU memory to prevent leaks + FreeImageData(data); + + return Resource::Load(); // Mark resource as successfully loaded + } +---- + +The loading pipeline follows a clear sequence that handles the complex transformation from file-based data to GPU resources. The file path construction assumes a standard naming convention that maps resource IDs to physical files, enabling consistent asset organization across the project. Using the KTX format provides several advantages including GPU-native format storage, mipmap support, and compression compatibility. + +Error handling at each stage prevents partial loading states that could leave the resource in an inconsistent condition. If image data loading fails, the function returns immediately without creating GPU resources, ensuring that the Texture object remains in a clean, unloaded state. This approach prevents resource leaks and makes error recovery more predictable for calling code. + +The temporary nature of the CPU-side image data reflects the typical texture loading workflow where pixel data is needed only long enough to upload to the GPU. Once the GPU resources are created and populated, the CPU copy can be safely discarded, reducing memory pressure and preventing unnecessary data duplication. + +=== Texture Resource: GPU Resource Cleanup and Memory Management + +Then, we implement comprehensive resource cleanup that ensures all GPU resources are properly released when the texture is no longer needed, preventing memory leaks in long-running applications. + +[source,cpp] +---- + void Unload() override { + // Only perform cleanup if resource is currently loaded + if (IsLoaded()) { + // Step 3a: Obtain device handle for resource destruction + vk::Device device = GetDevice(); + + // Step 3b: Destroy GPU objects in reverse creation order + // This ordering prevents use-after-free errors in GPU drivers + device.destroySampler(sampler); // Destroy sampling configuration + device.destroyImageView(imageView); // Destroy shader view + device.destroyImage(image); // Destroy image object + device.freeMemory(memory); // Release GPU memory allocation + + // Step 3c: Update base class state to reflect unloaded status + Resource::Unload(); + } + } + + // Public interface for accessing Vulkan resources safely + vk::Image GetImage() const { return image; } + vk::ImageView GetImageView() const { return imageView; } + vk::Sampler GetSampler() const { return sampler; } +---- + +The cleanup sequence follows Vulkan's object dependency requirements, where objects must be destroyed in reverse order of their creation to avoid validation errors and potential driver crashes. The sampler and image view depend on the image, so they must be destroyed first. The memory allocation is released last since it backs the image object. + +The conditional cleanup check prevents double-destruction errors that could occur if Unload() is called multiple times. This safety mechanism is particularly important in resource management systems where multiple code paths might trigger cleanup operations during error handling or shutdown sequences. + +The public getter interface provides controlled access to the internal Vulkan resources without exposing the implementation details or allowing external code to modify the resource state. This encapsulation ensures that the Texture object maintains complete control over its GPU resources throughout their lifetime. + +=== Texture Resource: Helper Methods and Implementation Details + +Finally, we provide the supporting infrastructure methods that handle the platform-specific details of image loading and Vulkan resource creation. + +[source,cpp] +---- +private: + unsigned char* LoadImageData(const std::string& filePath, int* width, int* height, int* channels) { + // Implementation using stb_image or ktx library + // This method abstracts the details of different image format support + // and provides a consistent interface for pixel data loading + // ... + return nullptr; // Placeholder + } + + void FreeImageData(unsigned char* data) { + // Implementation using stb_image or ktx library + // Ensures proper cleanup of image loader specific memory allocations + // Different libraries may require different cleanup approaches + // ... + } + + void CreateVulkanImage(unsigned char* data, int width, int height, int channels) { + // Implementation to create Vulkan image, allocate memory, and upload data + // This involves complex Vulkan operations including: + // - Format selection based on channel count and data type + // - Memory allocation with appropriate usage flags + // - Image creation with optimal tiling and layout + // - Data upload via staging buffers for efficiency + // - Image view creation for shader access + // - Sampler creation with appropriate filtering settings + // ... + } + + vk::Device GetDevice() { + // Get device from somewhere (e.g., singleton or parameter) + // Production code would use dependency injection or service location + // to provide the Vulkan device handle without tight coupling + // ... + return vk::Device(); // Placeholder + } +}; +---- + +The helper methods abstract away the platform-specific and library-specific details of texture loading and GPU resource creation. The `LoadImageData` method encapsulates support for different image formats and loading libraries, providing a consistent interface regardless of whether you're using STB Image, DevIL, FreeImage, or other image loading solutions. + +The `CreateVulkanImage` method represents one of the most complex operations in texture management, involving multiple Vulkan API calls with careful attention to format selection, memory alignment, and performance optimization. Production implementations typically use staging buffers for efficient data transfer and may include mipmap generation, format conversion, and compression support. + +The device access pattern shown here as a placeholder represents a common design challenge in resource management systems: how to provide access to core engine services without creating tight coupling. Production systems typically use dependency injection, service locators, or context objects to provide access to the Vulkan device and other core resources. + +=== Mesh Resource Implementation + +The Mesh resource represents the geometric foundation of 3D rendering, managing vertex and index data that define the shape and structure of 3D objects. This implementation demonstrates how to efficiently manage GPU buffer resources for geometric data. + +=== Mesh Resource: Geometric Data Structure and Buffer Management + +First, we establish the fundamental data structures required for storing and managing geometric data on the GPU, including both vertex attributes and index connectivity information. + +[source,cpp] +---- +// Mesh resource +class Mesh : public Resource { +private: + // Vertex data management - stores per-vertex attributes like position, normal, UV coordinates + vk::Buffer vertexBuffer; // GPU buffer containing vertex attribute data + vk::DeviceMemory vertexBufferMemory; // GPU memory backing the vertex buffer + uint32_t vertexCount = 0; // Number of vertices in this mesh + + // Index data management - defines triangle connectivity using vertex indices + vk::Buffer indexBuffer; // GPU buffer containing triangle index data + vk::DeviceMemory indexBufferMemory; // GPU memory backing the index buffer + uint32_t indexCount = 0; // Number of indices in this mesh (typically 3 per triangle) + +public: + explicit Mesh(const std::string& id) : Resource(id) {} + + ~Mesh() override { + Unload(); // Ensure GPU resources are cleaned up + } +---- + +The mesh resource architecture separates vertex and index data into distinct GPU buffers, following modern graphics API best practices. Vertex buffers contain per-vertex attributes such as positions, normals, texture coordinates, and color information, while index buffers define how vertices connect to form triangles. This separation enables efficient vertex reuse, where a single vertex can be referenced by multiple triangles, significantly reducing memory usage for typical 3D models. + +The buffer-memory pairing reflects Vulkan's explicit memory management model, where buffer objects and their backing memory allocations are managed separately. This approach provides fine-grained control over memory allocation strategies, enabling techniques like memory pooling, suballocation, and custom alignment requirements that can significantly impact rendering performance. + +The count tracking serves dual purposes: it provides essential information for rendering calls that specify how many vertices or indices to process, and it enables validation and debugging by allowing systems to verify that buffer contents match expected data sizes. + +=== Mesh Resource: Data Loading and Format Processing Pipeline + +Next, we implement the mesh loading pipeline that transforms file-based geometric data into GPU-ready buffer resources through format parsing and data validation. + +[source,cpp] +---- + bool Load() override { + // Step 2a: Construct file path using standardized naming convention + std::string filePath = "models/" + GetId() + ".gltf"; + + // Step 2b: Parse geometric data from file format into CPU-accessible structures + std::vector vertices; // Temporary CPU storage for vertex attributes + std::vector indices; // Temporary CPU storage for triangle indices + if (!LoadMeshData(filePath, vertices, indices)) { + return false; // Failed to parse file - abort loading + } + + // Step 2c: Transform CPU data into optimized GPU buffer resources + CreateVertexBuffer(vertices); // Upload vertex attributes to GPU + CreateIndexBuffer(indices); // Upload triangle connectivity to GPU + + // Step 2d: Cache metadata for efficient rendering operations + vertexCount = static_cast(vertices.size()); + indexCount = static_cast(indices.size()); + + return Resource::Load(); // Mark resource as successfully loaded + } +---- + +The loading pipeline follows a structured approach that separates file parsing from GPU resource creation, enabling better error handling and code reusability. The choice of glTF format provides several advantages including industry-standard mesh representation, embedded material information, and support for advanced features like skeletal animations and morph targets. + +The temporary CPU-side storage approach enables validation and processing of geometric data before committing to GPU resources. This intermediate step allows for mesh optimization techniques such as vertex cache optimization, triangle strip generation, or level-of-detail processing that can significantly improve rendering performance. + +The metadata caching strategy stores frequently accessed information locally to avoid expensive GPU queries during rendering. These counts are essential for draw calls, where the GPU needs to know exactly how many vertices to process and how many triangles to render, making local storage much more efficient than querying the GPU buffers repeatedly. + +=== Mesh Resource — Then: GPU Resource Cleanup and Memory Reclamation + +Then, we implement comprehensive cleanup that properly releases all GPU resources and memory allocations when the mesh is no longer needed, ensuring robust memory management in long-running applications. + +[source,cpp] +---- + void Unload() override { + // Only proceed with cleanup if resources are currently loaded + if (IsLoaded()) { + // Phase 3a: Obtain device handle for resource destruction + vk::Device device = GetDevice(); + + // Phase 3b: Destroy buffers and free GPU memory in proper sequence + // Index resources cleaned up first to maintain clear dependency order + device.destroyBuffer(indexBuffer); // Destroy index buffer object + device.freeMemory(indexBufferMemory); // Release index buffer memory + + // Vertex resources cleaned up second + device.destroyBuffer(vertexBuffer); // Destroy vertex buffer object + device.freeMemory(vertexBufferMemory); // Release vertex buffer memory + + // Phase 3c: Update base class state to reflect unloaded condition + Resource::Unload(); + } + } + + // Public interface for safe access to GPU resources and metadata + vk::Buffer GetVertexBuffer() const { return vertexBuffer; } + vk::Buffer GetIndexBuffer() const { return indexBuffer; } + uint32_t GetVertexCount() const { return vertexCount; } + uint32_t GetIndexCount() const { return indexCount; } +---- + +The cleanup sequence ensures that GPU resources are properly released without causing validation errors or driver instability. While Vulkan doesn't impose strict ordering requirements for buffer destruction, following a consistent pattern (index resources before vertex resources) makes the code more predictable and easier to debug when issues arise. + +The conditional cleanup check prevents double-destruction scenarios that could occur during error handling or when multiple systems attempt to clean up resources simultaneously. This safety mechanism is particularly important in complex rendering systems where resource ownership might be shared between multiple components. + +The public access interface provides controlled access to internal GPU resources while maintaining encapsulation. These getter methods enable rendering systems to bind the appropriate buffers for draw operations while preventing external code from accidentally modifying the mesh's internal state or triggering premature resource destruction. + +=== Mesh Resource: Helper Methods and Implementation Support Infrastructure + +The final phase provides the supporting methods that handle the complex details of mesh data parsing, buffer creation, and system integration required for complete mesh resource functionality. + +[source,cpp] +---- +private: + bool LoadMeshData(const std::string& filePath, std::vector& vertices, std::vector& indices) { + // Implementation using tinygltf or similar library + // This method handles the complex task of: + // - Opening and validating the mesh file format + // - Parsing vertex attributes (positions, normals, UVs, etc.) + // - Extracting index data that defines triangle connectivity + // - Converting from file format to engine-specific vertex structures + // - Performing validation to ensure data integrity + // ... + return true; // Placeholder + } + + void CreateVertexBuffer(const std::vector& vertices) { + // Implementation to create Vulkan buffer, allocate memory, and upload data + // This involves several complex Vulkan operations: + // - Calculating buffer size requirements based on vertex count and structure + // - Creating buffer with appropriate usage flags (vertex buffer usage) + // - Allocating GPU memory with optimal memory type selection + // - Uploading data via staging buffer for efficient transfer + // - Setting up memory barriers to ensure data availability + // ... + } + + void CreateIndexBuffer(const std::vector& indices) { + // Implementation to create Vulkan buffer, allocate memory, and upload data + // Similar to vertex buffer creation but optimized for index data: + // - Buffer creation with index buffer specific usage flags + // - Memory allocation optimized for read-heavy access patterns + // - Efficient data transfer using appropriate staging mechanisms + // - Index format validation (16-bit vs 32-bit indices) + // ... + } + + vk::Device GetDevice() { + // Get device from somewhere (e.g., singleton or parameter) + // Production implementations typically use dependency injection + // to avoid tight coupling between resource classes and core engine systems + // ... + return vk::Device(); // Placeholder + } +}; +---- + +The helper methods encapsulate the most complex aspects of mesh resource management, hiding implementation details while providing clean interfaces for the core loading and creation logic. The `LoadMeshData` method abstracts the intricacies of different mesh file formats and parsing libraries, enabling the resource system to support multiple formats through a consistent interface. + +The buffer creation methods represent some of the most performance-critical code in the mesh resource system, as inefficient GPU memory management can significantly impact rendering performance. Production implementations typically use staging buffers for data upload, implement memory pooling to reduce allocation overhead, and carefully select memory types based on GPU architecture characteristics. + +The device access pattern illustrates a common architectural challenge in resource management systems: balancing convenience with loose coupling. While direct access to global singletons can simplify implementation, production systems typically use dependency injection or service locator patterns to maintain testability and flexibility while providing access to core engine services. + +// Shader resource +class Shader : public Resource { +private: + vk::ShaderModule shaderModule; + vk::ShaderStageFlagBits stage; + +public: + Shader(const std::string& id, vk::ShaderStageFlagBits shaderStage) + : Resource(id), stage(shaderStage) {} + + ~Shader() override { + Unload(); + } + + bool Load() override { + // Determine file extension based on shader stage + std::string extension; + switch (stage) { + case vk::ShaderStageFlagBits::eVertex: extension = ".vert"; break; + case vk::ShaderStageFlagBits::eFragment: extension = ".frag"; break; + case vk::ShaderStageFlagBits::eCompute: extension = ".comp"; break; + default: return false; + } + + // Load shader from file + std::string filePath = "shaders/" + GetId() + extension + ".spv"; + + // Read shader code + std::vector shaderCode; + if (!ReadFile(filePath, shaderCode)) { + return false; + } + + // Create shader module + CreateShaderModule(shaderCode); + + return Resource::Load(); + } + + void Unload() override { + // Destroy Vulkan resources + if (IsLoaded()) { + // Get device from somewhere (e.g., singleton or parameter) + vk::Device device = GetDevice(); + + device.destroyShaderModule(shaderModule); + + Resource::Unload(); + } + } + + // Getters for Vulkan resources + vk::ShaderModule GetShaderModule() const { return shaderModule; } + vk::ShaderStageFlagBits GetStage() const { return stage; } + +private: + bool ReadFile(const std::string& filePath, std::vector& buffer) { + // Implementation to read binary file + // ... + return true; // Placeholder + } + + void CreateShaderModule(const std::vector& code) { + // Implementation to create Vulkan shader module + // ... + } + + vk::Device GetDevice() { + // Get device from somewhere (e.g., singleton or parameter) + // ... + return vk::Device(); // Placeholder + } +}; +---- + +=== Using the Resource Manager + +Here's how you might use the resource manager in your application: + +[source,cpp] +---- +// Create resource manager +ResourceManager resourceManager; + +// Load resources +auto texture = resourceManager.Load("brick"); +auto mesh = resourceManager.Load("cube"); +auto vertexShader = resourceManager.Load("basic", vk::ShaderStageFlagBits::eVertex); +auto fragmentShader = resourceManager.Load("basic", vk::ShaderStageFlagBits::eFragment); + +// Use resources +if (texture && mesh && vertexShader && fragmentShader) { + // Create material using shaders + Material material(vertexShader, fragmentShader); + + // Set texture in material + material.SetTexture("diffuse", texture); + + // Create entity with mesh and material + Entity entity("MyEntity"); + auto meshComponent = entity.AddComponent(mesh.Get(), &material); +} + +// Resources will be automatically released when handles go out of scope +// or you can explicitly release them +resourceManager.Release(texture.GetId()); +---- + +=== Advanced Resource Management Techniques + +==== Asynchronous Loading + +For large resources, it's often beneficial to load them asynchronously to avoid blocking the main thread: + +[source,cpp] +---- +class AsyncResourceManager { +private: + ResourceManager resourceManager; + std::thread workerThread; + std::queue> taskQueue; + std::mutex queueMutex; + std::condition_variable condition; + bool running = false; + +public: + AsyncResourceManager() { + Start(); + } + + ~AsyncResourceManager() { + Stop(); + } + + void Start() { + running = true; + workerThread = std::thread([this]() { + WorkerThread(); + }); + } + + void Stop() { + { + std::lock_guard lock(queueMutex); + running = false; + } + condition.notify_one(); + if (workerThread.joinable()) { + workerThread.join(); + } + } + + template + void LoadAsync(const std::string& resourceId, std::function)> callback) { + std::lock_guard lock(queueMutex); + taskQueue.push([this, resourceId, callback]() { + auto handle = resourceManager.Load(resourceId); + callback(handle); + }); + condition.notify_one(); + } + +private: + void WorkerThread() { + while (running) { + std::function task; + { + std::unique_lock lock(queueMutex); + condition.wait(lock, [this]() { + return !taskQueue.empty() || !running; + }); + + if (!running && taskQueue.empty()) { + return; + } + + task = std::move(taskQueue.front()); + taskQueue.pop(); + } + + task(); + } + } +}; + +// Usage example +AsyncResourceManager asyncResourceManager; + +asyncResourceManager.LoadAsync("large_texture", [](ResourceHandle texture) { + // This callback will be called when the texture is loaded + if (texture) { + std::cout << "Texture loaded successfully!" << std::endl; + } else { + std::cout << "Failed to load texture." << std::endl; + } +}); +---- + +==== Resource Streaming + +For very large resources like high-resolution textures or detailed meshes, you might want to implement streaming: + +1. *Level of Detail (LOD)* - Load lower-resolution versions first, then progressively load higher-resolution versions. +2. *Texture Streaming* - Load mipmap levels progressively, starting with the smallest. +3. *Mesh Streaming* - Load simplified versions of meshes first, then add detail. + +==== Hot Reloading + +During development, it's useful to be able to update resources without restarting the application: + +[source,cpp] +---- +class HotReloadResourceManager : public ResourceManager { +private: + std::unordered_map fileTimestamps; + std::thread watcherThread; + bool running = false; + +public: + HotReloadResourceManager() { + StartWatcher(); + } + + ~HotReloadResourceManager() { + StopWatcher(); + } + + void StartWatcher() { + running = true; + watcherThread = std::thread([this]() { + WatcherThread(); + }); + } + + void StopWatcher() { + running = false; + if (watcherThread.joinable()) { + watcherThread.join(); + } + } + + template + ResourceHandle Load(const std::string& resourceId) { + auto handle = ResourceManager::Load(resourceId); + + // Store file timestamp + std::string filePath = GetFilePath(resourceId); + try { + fileTimestamps[filePath] = std::filesystem::last_write_time(filePath); + } catch (const std::filesystem::filesystem_error& e) { + // File doesn't exist or can't be accessed + } + + return handle; + } + +private: + template + std::string GetFilePath(const std::string& resourceId) { + // Determine file path based on resource type and ID + if constexpr (std::is_same_v) { + return "textures/" + resourceId + ".ktx"; + } else if constexpr (std::is_same_v) { + return "models/" + resourceId + ".gltf"; + } else if constexpr (std::is_same_v) { + // Simplified for example + return "shaders/" + resourceId + ".spv"; + } else { + return ""; + } + } + + void WatcherThread() { + while (running) { + // Check for file changes + for (auto& [filePath, timestamp] : fileTimestamps) { + try { + auto currentTimestamp = std::filesystem::last_write_time(filePath); + if (currentTimestamp != timestamp) { + // File has changed, reload resource + ReloadResource(filePath); + timestamp = currentTimestamp; + } + } catch (const std::filesystem::filesystem_error& e) { + // File doesn't exist or can't be accessed + } + } + + // Sleep to avoid high CPU usage + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + } + + void ReloadResource(const std::string& filePath) { + // Extract resource ID and type from file path + // Reload the resource + // ... + } +}; +---- + +=== Conclusion + +A well-designed resource management system is crucial for efficiently handling assets in your rendering engine. By implementing the techniques described in this section, you can create a system that: + +1. Efficiently loads and unloads resources +2. Prevents redundant loading through caching +3. Manages memory usage through reference counting +4. Supports asynchronous loading for better performance +5. Enables hot reloading for faster development + +In the next section, we'll explore rendering pipeline design, which will build upon the resource management system to create a flexible and efficient rendering system. + +link:03_component_systems.adoc[Previous: Component Systems] | link:05_rendering_pipeline.adoc[Next: Rendering Pipeline] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc new file mode 100644 index 00000000..77715ee4 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/05_rendering_pipeline.adoc @@ -0,0 +1,1515 @@ +:pp: {plus}{plus} + += Engine Architecture: Rendering Pipeline + +== Rendering Pipeline + +A well-designed rendering pipeline is essential for creating a flexible and efficient rendering engine. In this section, we'll explore how to structure your rendering pipeline to support various rendering techniques and effects. + +=== Rendering Pipeline Overview + +The following diagram provides a high-level overview of a modern Vulkan rendering pipeline: + +image::../../../images/rendering_pipeline_flowchart.png[Rendering Pipeline Flowchart, width=600, alt="Flowchart showing the stages of a modern Vulkan rendering pipeline"] + +[NOTE] +==== +*Diagram Legend:* + +* *Boxes*: Represent the different stages of the rendering pipeline +* *Arrows*: Show the flow of data and execution between stages +* *Colors*: Different colors indicate different types of operations (processing, management, execution) +* *Supporting Components*: Rendergraphs and Synchronization primitives are shown as connected to the main pipeline flow +==== + +The rendering pipeline consists of several key stages: + +1. *Scene Culling* - Determine which objects are visible and need to be rendered. +2. *Render Pass Management* - Organize rendering into passes with specific purposes. +3. *Command Generation* - Generate commands for the GPU to execute. +4. *Execution* - Submit commands to the GPU for execution. +5. *Post-Processing* - Apply effects to the rendered image. + +Supporting components like Rendergraphs help manage dependencies between render passes, while Synchronization primitives ensure correct execution order. Different rendering techniques (Deferred, Forward+, PBR) can be implemented within this pipeline architecture. + +=== Rendering Pipeline Challenges + +When designing a rendering pipeline, you'll need to address several challenges: + +1. *Flexibility* - Support different rendering techniques and effects. +2. *Performance* - Efficiently utilize the GPU and minimize state changes. +3. *Extensibility* - Allow for easy addition of new rendering features. +4. *Maintainability* - Keep the code organized and easy to understand. +5. *Platform Independence* - Abstract away platform-specific details. + +=== Rendering Pipeline Architecture + +Earlier we outlined the major stages of a modern pipeline. Rather than repeating that list, we'll now dive into each stage, focusing on responsibilities, data flow, and practical implementation patterns that keep the engine flexible and performant. + +=== Scene Culling + +Before rendering, we need to determine which objects are visible to the camera. This process is called culling and can significantly improve performance by reducing the number of objects that need to be rendered. + +[source,cpp] +---- +class CullingSystem { +private: + Camera* camera; + std::vector visibleEntities; + +public: + explicit CullingSystem(Camera* cam) : camera(cam) {} + + void SetCamera(Camera* cam) { + camera = cam; + } + + void CullScene(const std::vector& allEntities) { + visibleEntities.clear(); + + if (!camera) return; + + // Get camera frustum + Frustum frustum = camera->GetFrustum(); + + // Check each entity against the frustum + for (auto entity : allEntities) { + if (!entity->IsActive()) continue; + + auto meshComponent = entity->GetComponent(); + if (!meshComponent) continue; + + auto transformComponent = entity->GetComponent(); + if (!transformComponent) continue; + + // Get bounding box of the mesh + BoundingBox boundingBox = meshComponent->GetBoundingBox(); + + // Transform bounding box by entity transform + boundingBox.Transform(transformComponent->GetTransformMatrix()); + + // Check if bounding box is visible + if (frustum.Intersects(boundingBox)) { + visibleEntities.push_back(entity); + } + } + } + + const std::vector& GetVisibleEntities() const { + return visibleEntities; + } +}; +---- + +=== Render Pass Management + +Modern rendering techniques often require multiple passes, each with a specific purpose. A render pass manager helps organize these passes and their dependencies. + +In this tutorial, we use Vulkan's dynamic rendering feature with vk::raii instead of traditional render passes. Dynamic rendering simplifies the rendering process by allowing us to begin and end rendering operations with a single command, without explicitly creating VkRenderPass and VkFramebuffer objects. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Additionally, our engine uses C++20 modules for better code organization, faster compilation times, and improved encapsulation. + +=== Rendergraphs and Synchronization + +A rendergraph is a higher-level abstraction that represents the entire rendering process as a directed acyclic graph (DAG), where nodes are render passes and edges represent dependencies between them. This approach offers several advantages over traditional render pass management: + +==== What is a Rendergraph? + +A rendergraph is a data structure that: + +1. *Describes Resources*: Tracks all resources (textures, buffers) used in rendering. +2. *Defines Operations*: Specifies what operations (render passes) will be performed. +3. *Manages Dependencies*: Automatically determines the dependencies between operations. +4. *Handles Synchronization*: Automatically inserts necessary synchronization primitives. +5. *Optimizes Memory*: Can perform memory aliasing and other optimizations. + +=== Rendergraph: Data Structure Architecture and Resource Representation + +First, we need to establish the fundamental data structures that represent rendering resources and passes within the rendergraph system. + +[source,cpp] +---- +// A comprehensive rendergraph implementation for automated dependency management +class Rendergraph { +private: + // Resource description and management structure + // Represents any GPU resource used during rendering (textures, render targets, buffers) + struct Resource { + std::string name; // Human-readable identifier for debugging and referencing + vk::Format format; // Pixel format (RGBA8, Depth24Stencil8, etc.) + vk::Extent2D extent; // Dimensions in pixels for 2D resources + vk::ImageUsageFlags usage; // How this resource will be used (color attachment, texture, etc.) + vk::ImageLayout initialLayout; // Expected layout when the frame begins + vk::ImageLayout finalLayout; // Required layout when the frame ends + + // Actual GPU resources - populated during compilation + vk::raii::Image image = nullptr; // The GPU image object + vk::raii::DeviceMemory memory = nullptr; // Backing memory allocation + vk::raii::ImageView view = nullptr; // Shader-accessible view of the image + }; + + // Render pass representation within the graph structure + // Each pass represents a distinct rendering operation with defined inputs and outputs + struct Pass { + std::string name; // Descriptive name for debugging and profiling + std::vector inputs; // Resources this pass reads from (dependencies) + std::vector outputs; // Resources this pass writes to (products) + std::function executeFunc; // The actual rendering code + }; + + // Core data storage for the rendergraph system + std::unordered_map resources; // All resources referenced in the graph + std::vector passes; // All rendering passes in definition order + std::vector executionOrder; // Computed optimal execution sequence + + // Automatic synchronization management + // These objects ensure correct GPU execution order without manual barriers + std::vector semaphores; // GPU synchronization primitives + std::vector> semaphoreSignalWaitPairs; // (signaling pass, waiting pass) + + vk::raii::Device& device; // Vulkan device for resource creation + +public: + explicit Rendergraph(vk::raii::Device& dev) : device(dev) {} +---- + +The data structure architecture reflects the core philosophy of rendergraphs: treating rendering as a series of transformations on resources rather than imperative GPU commands. The Resource structure encapsulates everything needed to create and manage GPU resources, while the Pass structure defines rendering operations in terms of their resource dependencies rather than their implementation details. + +This approach enables powerful optimizations like automatic memory aliasing (where multiple resources share the same memory if their lifetimes don't overlap) and optimal resource layout transitions. The separation between resource description and actual GPU objects allows the rendergraph to make informed decisions about resource management during the compilation phase. + +=== Rendergraph: Resource Registration and Pass Definition Interface + +Now for the public interface for building the rendergraph by registering resources and defining rendering passes with their dependencies. + +[source,cpp] +---- + // Resource registration interface for declaring all resources used during rendering + // This method establishes resource metadata without creating actual GPU resources + void AddResource(const std::string& name, vk::Format format, vk::Extent2D extent, + vk::ImageUsageFlags usage, vk::ImageLayout initialLayout, + vk::ImageLayout finalLayout) { + Resource resource; + resource.name = name; // Store human-readable identifier + resource.format = format; // Define pixel format and bit depth + resource.extent = extent; // Set resource dimensions + resource.usage = usage; // Specify intended usage patterns + resource.initialLayout = initialLayout; // Define starting layout state + resource.finalLayout = finalLayout; // Define required ending state + + resources[name] = resource; // Register in the global resource map + } + + // Pass registration interface for defining rendering operations and their dependencies + // This method establishes the logical structure of rendering without immediate execution + void AddPass(const std::string& name, + const std::vector& inputs, + const std::vector& outputs, + std::function executeFunc) { + Pass pass; + pass.name = name; // Assign descriptive identifier + pass.inputs = inputs; // List all resources this pass reads + pass.outputs = outputs; // List all resources this pass writes + pass.executeFunc = executeFunc; // Store the actual rendering implementation + + passes.push_back(pass); // Add to the ordered pass list + } +---- + +The registration interface enables declarative rendergraph construction where developers specify what they want to achieve rather than how to achieve it. This high-level approach allows the rendergraph to analyze the entire rendering pipeline before making resource allocation and scheduling decisions. + +The deferred execution model (where passes store function objects rather than immediate GPU commands) enables powerful compile-time optimizations. The rendergraph can reorder passes, merge compatible operations, and optimize resource usage based on the complete dependency graph rather than making local decisions for each pass. + +=== Rendergraph: Dependency Analysis and Execution Ordering + +Now we implement the core algorithmic logic that analyzes pass dependencies and computes an optimal execution order for the rendering pipeline. + +[source,cpp] +---- + // Rendergraph compilation - transforms declarative descriptions into executable pipeline + // This method performs dependency analysis, resource allocation, and execution planning + void Compile() { + // Dependency Graph Construction + // Build bidirectional dependency relationships between passes + std::vector> dependencies(passes.size()); // What each pass depends on + std::vector> dependents(passes.size()); // What depends on each pass + + // Track which pass produces each resource (write-after-write dependencies) + std::unordered_map resourceWriters; + + // Dependency Discovery Through Resource Usage Analysis + // Analyze each pass to determine data flow relationships + for (size_t i = 0; i < passes.size(); ++i) { + const auto& pass = passes[i]; + + // Process input dependencies - this pass must wait for producers + for (const auto& input : pass.inputs) { + auto it = resourceWriters.find(input); + if (it != resourceWriters.end()) { + // Found the pass that produces this input - create dependency link + dependencies[i].push_back(it->second); // This pass depends on the producer + dependents[it->second].push_back(i); // Producer has this as dependent + } + } + + // Register output production - subsequent passes may depend on these + for (const auto& output : pass.outputs) { + resourceWriters[output] = i; // Record this pass as producer + } + } + + // Topological Sort for Optimal Execution Order + // Use depth-first search to compute valid execution sequence while detecting cycles + std::vector visited(passes.size(), false); // Track completed nodes + std::vector inStack(passes.size(), false); // Track current recursion path + + std::function visit = [&](size_t node) { + if (inStack[node]) { + // Cycle detection - circular dependency found + throw std::runtime_error("Cycle detected in rendergraph"); + } + + if (visited[node]) { + return; // Already processed this node and its dependencies + } + + inStack[node] = true; // Mark as currently being processed + + // Recursively process all dependent passes first (post-order traversal) + for (auto dependent : dependents[node]) { + visit(dependent); + } + + inStack[node] = false; // Remove from current path + visited[node] = true; // Mark as completely processed + executionOrder.push_back(node); // Add to execution sequence + }; + + // Process all unvisited nodes to handle disconnected graph components + for (size_t i = 0; i < passes.size(); ++i) { + if (!visited[i]) { + visit(i); + } + } +---- + +The dependency analysis represents the mathematical core of the rendergraph system, transforming an abstract description of rendering operations into a concrete execution plan. The bidirectional dependency tracking enables efficient graph traversal algorithms and provides the information needed for automatic synchronization. + +The topological sort algorithm ensures that passes execute in dependency order while detecting impossible circular dependencies that would represent logical errors in the rendering pipeline design. This compile-time validation catches many common rendering pipeline bugs before they manifest as runtime GPU synchronization issues. + +=== Rendergraph: Automatic Synchronization and Resource Allocation + +Next create the GPU synchronization objects needed for correct execution ordering and allocates the actual Vulkan resources for all registered resources. + +[source,cpp] +---- + // Automatic Synchronization Object Creation + // Generate semaphores for all dependencies identified during analysis + for (size_t i = 0; i < passes.size(); ++i) { + for (auto dep : dependencies[i]) { + // Create a GPU semaphore for this dependency relationship + // The dependent pass will wait on this semaphore before executing + semaphores.emplace_back(device.createSemaphore({})); + semaphoreSignalWaitPairs.emplace_back(dep, i); // (producer, consumer) pair + } + } + + // Physical Resource Allocation and Creation + // Transform resource descriptions into actual GPU objects + for (auto& [name, resource] : resources) { + // Configure image creation parameters based on resource description + vk::ImageCreateInfo imageInfo; + imageInfo.setImageType(vk::ImageType::e2D) // 2D texture/render target + .setFormat(resource.format) // Pixel format from description + .setExtent({resource.extent.width, resource.extent.height, 1}) // Dimensions + .setMipLevels(1) // Single mip level for simplicity + .setArrayLayers(1) // Single layer (not array texture) + .setSamples(vk::SampleCountFlagBits::e1) // No multisampling + .setTiling(vk::ImageTiling::eOptimal) // GPU-optimal memory layout + .setUsage(resource.usage) // Usage flags from registration + .setSharingMode(vk::SharingMode::eExclusive) // Single queue family access + .setInitialLayout(vk::ImageLayout::eUndefined); // Initial layout (will be transitioned) + + resource.image = device.createImage(imageInfo); // Create the GPU image object + + // Allocate backing memory for the image + vk::MemoryRequirements memRequirements = resource.image.getMemoryRequirements(); + + vk::MemoryAllocateInfo allocInfo; + allocInfo.setAllocationSize(memRequirements.size) // Required memory size + .setMemoryTypeIndex(FindMemoryType(memRequirements.memoryTypeBits, + vk::MemoryPropertyFlagBits::eDeviceLocal)); // GPU-local memory + + resource.memory = device.allocateMemory(allocInfo); // Allocate GPU memory + resource.image.bindMemory(*resource.memory, 0); // Bind memory to image + + // Create image view for shader access + vk::ImageViewCreateInfo viewInfo; + viewInfo.setImage(*resource.image) // Reference the created image + .setViewType(vk::ImageViewType::e2D) // 2D view type + .setFormat(resource.format) // Match image format + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); // Full image access + + resource.view = device.createImageView(viewInfo); // Create shader-accessible view + } + } + + // Resource access interface for retrieving compiled resources + Resource* GetResource(const std::string& name) { + auto it = resources.find(name); + return (it != resources.end()) ? &it->second : nullptr; + } +---- + +=== Rendergraph: Execution Engine and Command Recording + +Finally, implement the execution engine that coordinates pass execution with proper synchronization and resource transitions. + +[source,cpp] +---- + + // Rendergraph execution engine - coordinates pass execution with automatic synchronization + // This method transforms the compiled rendergraph into actual GPU work + void Execute(vk::raii::CommandBuffer& commandBuffer, vk::Queue queue) { + // Execution state management for dynamic synchronization + std::vector cmdBuffers; // Command buffer storage + std::vector waitSemaphores; // Synchronization dependencies for current pass + std::vector waitStages; // Pipeline stages to wait on + std::vector signalSemaphores; // Semaphores to signal after current pass + + // Ordered Pass Execution with Automatic Dependency Management + // Execute each pass in the computed dependency-safe order + for (auto passIdx : executionOrder) { + const auto& pass = passes[passIdx]; + + // Synchronization Setup - Collect Dependencies for Current Pass + // Determine what this pass must wait for before executing + waitSemaphores.clear(); + waitStages.clear(); + + for (size_t i = 0; i < semaphoreSignalWaitPairs.size(); ++i) { + if (semaphoreSignalWaitPairs[i].second == passIdx) { + // This pass depends on the completion of another pass + waitSemaphores.push_back(*semaphores[i]); // Wait for dependency completion + waitStages.push_back(vk::PipelineStageFlagBits::eColorAttachmentOutput); // Wait at output stage + } + } + + // Collect semaphores that this pass will signal for dependent passes + signalSemaphores.clear(); + for (size_t i = 0; i < semaphoreSignalWaitPairs.size(); ++i) { + if (semaphoreSignalWaitPairs[i].first == passIdx) { + // Other passes depend on this pass's completion + signalSemaphores.push_back(*semaphores[i]); // Signal completion for dependents + } + } + + // Command Buffer Preparation and Resource Layout Transitions + // Set up command recording and transition resources to appropriate layouts + commandBuffer.begin({}); // Begin command recording + + // Transition input resources to shader-readable layouts + for (const auto& input : pass.inputs) { + auto& resource = resources[input]; + + vk::ImageMemoryBarrier barrier; + barrier.setOldLayout(resource.initialLayout) // Current resource layout + .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal) // Target layout for reading + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) // No queue family transfer + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setImage(*resource.image) // Target image + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) // Full image range + .setSrcAccessMask(vk::AccessFlagBits::eMemoryWrite) // Previous write access + .setDstAccessMask(vk::AccessFlagBits::eShaderRead); // Required read access + + // Insert pipeline barrier for safe layout transition + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eAllCommands, // Wait for all previous work + vk::PipelineStageFlagBits::eFragmentShader, // Enable fragment shader access + vk::DependencyFlagBits::eByRegion, // Region-local dependency + 0, nullptr, 0, nullptr, 1, &barrier // Image barrier only + ); + } + + // Transition output resources to render target layouts + for (const auto& output : pass.outputs) { + auto& resource = resources[output]; + + vk::ImageMemoryBarrier barrier; + barrier.setOldLayout(resource.initialLayout) // Current layout state + .setNewLayout(vk::ImageLayout::eColorAttachmentOptimal) // Optimal for color output + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setImage(*resource.image) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) + .setSrcAccessMask(vk::AccessFlagBits::eMemoryRead) // Previous read access + .setDstAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); // Required write access + + // Insert barrier for safe transition to writable state + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eAllCommands, + vk::PipelineStageFlagBits::eColorAttachmentOutput, // Enable color attachment writes + vk::DependencyFlagBits::eByRegion, + 0, nullptr, 0, nullptr, 1, &barrier + ); + } + + // Pass Execution - Execute the Actual Rendering Logic + // Call the user-provided rendering function with prepared command buffer + pass.executeFunc(commandBuffer); // Execute pass-specific rendering + + // Final Layout Transitions - Prepare Resources for Subsequent Use + // Transition output resources to their final required layouts + for (const auto& output : pass.outputs) { + auto& resource = resources[output]; + + vk::ImageMemoryBarrier barrier; + barrier.setOldLayout(vk::ImageLayout::eColorAttachmentOptimal) // Current writable layout + .setNewLayout(resource.finalLayout) // Required final layout + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) + .setImage(*resource.image) + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}) + .setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite) // Previous write operations + .setDstAccessMask(vk::AccessFlagBits::eMemoryRead); // Enable subsequent reads + + // Insert final barrier for layout transition + commandBuffer.pipelineBarrier( + vk::PipelineStageFlagBits::eColorAttachmentOutput, // After color writes complete + vk::PipelineStageFlagBits::eAllCommands, // Before any subsequent work + vk::DependencyFlagBits::eByRegion, + 0, nullptr, 0, nullptr, 1, &barrier + ); + } + + // Command Submission with Synchronization + // Submit command buffer with appropriate dependency and signaling semaphores + commandBuffer.end(); // Finalize command recording + + vk::SubmitInfo submitInfo; + submitInfo.setWaitSemaphoreCount(static_cast(waitSemaphores.size())) // Dependencies to wait for + .setPWaitSemaphores(waitSemaphores.data()) // Dependency semaphores + .setPWaitDstStageMask(waitStages.data()) // Pipeline stages to wait at + .setCommandBufferCount(1) // Single command buffer + .setPCommandBuffers(&*commandBuffer) // Command buffer to execute + .setSignalSemaphoreCount(static_cast(signalSemaphores.size())) // Semaphores to signal + .setPSignalSemaphores(signalSemaphores.data()); // Signal semaphores + + queue.submit(1, &submitInfo, nullptr); // Submit to GPU queue + } + } +---- + +The execution engine represents the culmination of the rendergraph system, where all the analysis and preparation work pays off in coordinated GPU execution. The automatic synchronization ensures that passes execute in the correct order without manual barrier management, while the automatic layout transitions handle the complex state management that Vulkan requires for optimal performance. + +This execution model demonstrates the power of the rendergraph abstraction: complex multi-pass rendering with dozens of resources and dependencies gets reduced to a simple `Execute()` call, with all the synchronization and resource management handled automatically based on the declarative pass and resource descriptions. + +[source,cpp] +---- +private: + uint32_t FindMemoryType(uint32_t typeFilter, vk::MemoryPropertyFlags properties) { + // Implementation to find suitable memory type + // ... + return 0; // Placeholder + } +}; +---- + +==== Vulkan Synchronization + +Synchronization in Vulkan is one of the most complicated topics. Vulkan provides several synchronization primitives to ensure correct execution order and memory visibility: + +1. *Semaphores*: Used for synchronization between queue operations (GPU-GPU synchronization). +2. *Fences*: Used for synchronization between CPU and GPU. +3. *Events*: Used for fine-grained synchronization within a command buffer. +4. *Barriers*: Used to synchronize access to resources and perform layout transitions. + +Proper synchronization is crucial in Vulkan because: + +1. *No Implicit Synchronization*: Unlike OpenGL, Vulkan doesn't provide implicit synchronization between operations. +2. *Parallel Execution*: Modern GPUs execute commands in parallel, which can lead to race conditions without proper synchronization. +3. *Memory Visibility*: Changes made by one operation may not be visible to another without proper barriers. + +The vulkan tutorial includes a more detailed discussion of synchronization, the proper uses of the primitives described above. + +* xref:../../03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.adoc[Synchronization] +* xref:../../03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.adoc[Frames In Flight] +* xref:../../11_Compute_Shader.adoc[Compute Shader] +* xref:../../17_Multithreading.adoc[Multithreading] + + +===== Pipeline Barriers + +Pipeline barriers are one of the most important synchronization primitives in Vulkan. They ensure that operations before the barrier are complete before operations after the barrier begin, and they can also perform layout transitions for images. Let's examine how to implement proper image layout transitions through a comprehensive breakdown of the process. + +=== Image Layout Transition: Barrier Configuration and Resource Specification + +First, we establish the basic barrier structure and identify which image resource needs to transition between layouts. + +[source,cpp] +---- +// Comprehensive image layout transition implementation +// This function demonstrates proper synchronization and layout management in Vulkan +void TransitionImageLayout(vk::raii::CommandBuffer& commandBuffer, + vk::Image image, + vk::Format format, + vk::ImageLayout oldLayout, + vk::ImageLayout newLayout) { + + // Configure the basic image memory barrier structure + // This barrier will coordinate memory access and layout transitions + vk::ImageMemoryBarrier barrier; + barrier.setOldLayout(oldLayout) // Current image layout state + .setNewLayout(newLayout) // Target layout after transition + .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) // No queue family ownership transfer + .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED) // Same queue family throughout + .setImage(image) // Target image for the transition + .setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}); // Full color image range +---- + +The image memory barrier serves as the fundamental mechanism for coordinating both memory access patterns and image layout transitions in Vulkan. Unlike OpenGL where these operations happen automatically, Vulkan requires explicit specification of when and how image layouts change. The queue family settings using VK_QUEUE_FAMILY_IGNORED indicate that we're not transferring ownership between different queue families, which simplifies the synchronization requirements. + +The subresource range specification defines exactly which portions of the image are affected by this barrier. In this case, we're transitioning the entire color aspect of the image across all mip levels and array layers, which is the most common scenario for basic texture operations. + +=== Image Layout Transition: Pipeline Stage and Access Mask Determination + +Next, we analyze the specific layout transition being performed and determine the appropriate pipeline stages and memory access patterns for optimal synchronization. + +[source,cpp] +---- + // Initialize pipeline stage tracking for synchronization timing + // These stages define when operations must complete and when new operations can begin + vk::PipelineStageFlags sourceStage; // When previous operations must finish + vk::PipelineStageFlags destinationStage; // When subsequent operations can start + + // Configure synchronization for undefined-to-transfer layout transitions + // This pattern is common when preparing images for data uploads + if (oldLayout == vk::ImageLayout::eUndefined && + newLayout == vk::ImageLayout::eTransferDstOptimal) { + + // Configure memory access permissions for upload preparation + barrier.setSrcAccessMask(vk::AccessFlagBits::eNone) // No previous access to synchronize + .setDstAccessMask(vk::AccessFlagBits::eTransferWrite); // Enable transfer write operations + + // Set pipeline stage synchronization points for upload workflow + sourceStage = vk::PipelineStageFlagBits::eTopOfPipe; // No previous work to wait for + destinationStage = vk::PipelineStageFlagBits::eTransfer; // Transfer operations can proceed + + // Configure synchronization for transfer-to-shader layout transitions + // This pattern prepares uploaded images for shader sampling + } else if (oldLayout == vk::ImageLayout::eTransferDstOptimal && + newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { + + // Configure memory access transition from writing to reading + barrier.setSrcAccessMask(vk::AccessFlagBits::eTransferWrite) // Previous transfer writes must complete + .setDstAccessMask(vk::AccessFlagBits::eShaderRead); // Enable shader read access + + // Set pipeline stage synchronization for shader usage workflow + sourceStage = vk::PipelineStageFlagBits::eTransfer; // Transfer operations must complete + destinationStage = vk::PipelineStageFlagBits::eFragmentShader; // Fragment shaders can access + + } else { + // Handle unsupported transition combinations + // Production code would include additional common transition patterns + throw std::invalid_argument("Unsupported layout transition!"); + } +---- + +The pipeline stage and access mask configuration represents the heart of Vulkan's explicit synchronization model. By specifying exactly which operations must complete before the barrier (source stage) and which operations can begin after the barrier (destination stage), we create precise control over GPU execution timing without unnecessary stalls. + +The access mask patterns define the memory visibility requirements for each transition. The transition from "no access" to "transfer write" enables efficient image upload without waiting for non-existent previous operations. The transition from "transfer write" to "shader read" ensures that uploaded data is fully written and visible before shaders attempt to sample from the texture. + +=== Image Layout Transition: Barrier Execution and GPU Synchronization + +Finally, we submit the configured barrier to the GPU command stream, ensuring that the layout transition and synchronization occur at the correct point in the rendering pipeline. + +[source,cpp] +---- + // Execute the pipeline barrier with configured synchronization + // This commits the layout transition and memory synchronization to the command buffer + commandBuffer.pipelineBarrier( + sourceStage, // Wait for these operations to complete + destinationStage, // Before allowing these operations to begin + vk::DependencyFlagBits::eByRegion, // Enable region-local optimizations + 0, nullptr, // No memory barriers needed + 0, nullptr, // No buffer barriers needed + 1, &barrier // Apply our image memory barrier + ); +} +---- + +The pipeline barrier submission represents the culmination of our synchronization planning, where the configured barrier becomes part of the GPU's command stream. The `ByRegion` dependency flag enables GPU optimizations for cases where different regions of the image can be processed independently, potentially improving performance on tile-based renderers and other advanced GPU architectures. + +The parameter structure clearly separates different types of barriers (memory, buffer, and image), allowing the GPU driver to apply the most efficient synchronization strategy for each resource type. In our case, we only need image barrier synchronization, so the other barrier arrays remain empty, avoiding unnecessary overhead. + +==== Semaphores and Fences + +Semaphores and fences are used for coarser-grained synchronization between different stages of the rendering pipeline and between CPU and GPU operations. Let's examine how to properly coordinate frame rendering using these synchronization primitives through a comprehensive breakdown of the frame rendering process. + +=== Frame Rendering: CPU-GPU Synchronization and Frame Pacing + +First, we ensure proper coordination between CPU frame preparation and GPU execution, preventing the CPU from getting too far ahead of the GPU and managing resource contention. + +[source,cpp] +---- +// Comprehensive frame rendering with proper synchronization +// This function demonstrates the complete cycle of frame rendering coordination +void RenderFrame(vk::raii::Device& device, vk::Queue graphicsQueue, vk::Queue presentQueue) { + + // Synchronize with previous frame completion + // Prevent CPU from submitting work faster than GPU can process it + vk::Result result = device.waitForFences(1, &*inFlightFence, VK_TRUE, UINT64_MAX); + + // Reset fence for this frame's completion tracking + // Prepare the fence to signal when this frame's GPU work completes + device.resetFences(1, &*inFlightFence); +---- + +The fence-based synchronization serves as the primary mechanism for CPU-GPU coordination in frame rendering. By waiting for the previous frame's fence, we ensure that the GPU has completed processing the previous frame before beginning work on the current frame. This prevents the CPU from overwhelming the GPU with work and helps maintain stable frame pacing. + +The fence reset operation prepares the synchronization object for the current frame. Fences are binary signals that transition from unsignaled to signaled state when associated GPU work completes, so they must be explicitly reset before reuse. The timeout value UINT64_MAX effectively means "wait indefinitely," which is appropriate for frame synchronization where we must ensure completion. + +=== Frame Rendering: Swapchain Image Acquisition and Resource Preparation + +Next, we acquire the next available swapchain image for rendering, coordinating with the presentation engine to ensure proper image availability. + +[source,cpp] +---- + // Acquire next available image from the swapchain + // This operation coordinates with the presentation engine and display system + uint32_t imageIndex; + result = device.acquireNextImageKHR(*swapchain, // Target swapchain for acquisition + UINT64_MAX, // Wait indefinitely for image availability + *imageAvailableSemaphore, // Semaphore signaled when image is available + nullptr, // No fence needed for this operation + &imageIndex); // Receives index of acquired image + + // Record command buffer for this frame's rendering + // Command buffer recording happens here with acquired image as render target + // ... (command recording implementation would go here) +---- + +The swapchain image acquisition represents a critical synchronization point between the rendering system and the presentation engine. The operation may block if no images are currently available (for example, if all swapchain images are being displayed or processed), making it essential for frame pacing. The semaphore signaled by this operation will be used later to ensure that rendering doesn't begin until the acquired image is truly available for modification. + +The indefinite timeout ensures that acquisition will eventually succeed even under heavy load or when dealing with variable refresh rate displays. The acquired image index determines which swapchain image becomes the render target for this frame, affecting descriptor set bindings and render pass configuration in the subsequent command recording phase. + +=== Frame Rendering: GPU Work Submission and Inter-Queue Synchronization + +Next, we submit the recorded rendering commands to the GPU with proper synchronization to coordinate between image acquisition, rendering, and presentation operations. + +[source,cpp] +---- + // Configure GPU work submission with comprehensive synchronization + // This submission coordinates image availability, rendering, and presentation readiness + vk::SubmitInfo submitInfo; + vk::PipelineStageFlags waitStages[] = {vk::PipelineStageFlagBits::eColorAttachmentOutput}; + + submitInfo.setWaitSemaphoreCount(1) // Wait for one semaphore before execution + .setPWaitSemaphores(&*imageAvailableSemaphore) // Don't start until image is available + .setPWaitDstStageMask(waitStages) // Specifically wait before color output + .setCommandBufferCount(1) // Submit one command buffer + .setPCommandBuffers(&*commandBuffer) // The recorded rendering commands + .setSignalSemaphoreCount(1) // Signal one semaphore when complete + .setPSignalSemaphores(&*renderFinishedSemaphore); // Notify when rendering is finished + + // Submit work to GPU with fence-based completion tracking + // The fence allows CPU to know when this frame's GPU work has completed + graphicsQueue.submit(1, &submitInfo, *inFlightFence); +---- + +The submission configuration demonstrates Vulkan's explicit synchronization model for coordinating multiple GPU operations. The wait semaphore ensures that rendering commands don't execute until the swapchain image is actually available for modification. The wait stage mask specifies exactly which part of the graphics pipeline must wait—in this case, color attachment output—allowing earlier pipeline stages to proceed if they don't depend on the swapchain image. + +The signal semaphore communicates completion of rendering work to other operations that depend on the rendered result, such as presentation. The fence provides CPU-visible completion notification, enabling the frame pacing logic we saw in earlier. This three-way synchronization (wait semaphore, signal semaphore, and fence) creates a complete coordination system for the frame rendering pipeline. + +=== Frame Rendering: Presentation Coordination and Display Integration + +Finally, we coordinate with the presentation engine to display the rendered frame, ensuring that presentation waits for rendering completion and handles the transition from rendering to display. + +[source,cpp] +---- + // Present the rendered image to the display + // This operation transfers the completed frame from rendering to display system + vk::PresentInfoKHR presentInfo; + presentInfo.setWaitSemaphoreCount(1) // Wait for rendering completion + .setPWaitSemaphores(&*renderFinishedSemaphore) // Don't present until rendering finishes + .setSwapchainCount(1) // Present to one swapchain + .setPSwapchains(&*swapchain) // Target swapchain for presentation + .setPImageIndices(&imageIndex); // Present the image we rendered to + + // Submit presentation request to the presentation engine + result = presentQueue.presentKHR(&presentInfo); +} +---- + +The presentation phase completes the frame rendering cycle by coordinating the transfer from rendering to display. The wait semaphore ensures that presentation doesn't begin until all rendering operations have completed, preventing the display of partially rendered frames. This synchronization is crucial because presentation and rendering may occur on different GPU queues with different timing characteristics. + +The presentation operation itself is asynchronous—it queues the presentation request and returns immediately, allowing the CPU to begin preparing the next frame. The presentation engine handles the actual coordination with the display hardware, including timing synchronization with refresh rates and managing the transition of the swapchain image from "rendering" to "displaying" to "available for reuse" states. + +==== How Rendergraphs Help with Synchronization + +Rendergraphs simplify synchronization by: + +1. *Automatic Dependency Tracking*: The rendergraph knows which passes depend on which resources, so it can automatically insert the necessary synchronization primitives. +2. *Optimal Barrier Placement*: The rendergraph can analyze the entire rendering process and place barriers only where needed, reducing overhead. +3. *Layout Transitions*: The rendergraph can automatically handle image layout transitions based on how resources are used. +4. *Resource Aliasing*: The rendergraph can reuse memory for resources that aren't used simultaneously, reducing memory usage. + +==== Dynamic Rendering and Its Integration with Rendergraphs + +Dynamic rendering is a modern Vulkan feature that simplifies the rendering process and works particularly well with rendergraphs. Before diving into implementation examples, let's understand what dynamic rendering is and how it relates to our rendering pipeline architecture. + +===== Benefits of Dynamic Rendering + +Dynamic rendering offers several advantages over traditional render passes: + +1. *Simplified API*: No need to create and manage VkRenderPass and VkFramebuffer objects, reducing code complexity. +2. *More Flexible Rendering*: Easier to change render targets and attachment formats at runtime. +3. *Improved Compatibility*: Works better with modern rendering techniques that don't fit well into the traditional render pass model. +4. *Reduced State Management*: Fewer objects to track and synchronize. +5. *Easier Debugging*: Simpler rendering code is easier to debug and maintain. + +With dynamic rendering, we specify all rendering states (render targets, +load/store operations, etc.) directly within the vkCmdBeginRendering call, +rather than setting it up ahead of time in a VkRenderPass object. This allows for more dynamic rendering workflows and simplifies the implementation of techniques like deferred rendering. + +===== Dynamic Rendering in Rendergraphs + +When combined with rendergraphs, dynamic rendering becomes even more powerful. The rendergraph handles the resource dependencies and synchronization, while dynamic rendering simplifies the actual rendering process. This combination provides both flexibility and performance. + +===== Example: Implementing a Deferred Renderer with a Rendergraph and Dynamic Rendering + +Deferred rendering represents a sophisticated rendering technique that separates geometry processing from lighting calculations, enabling efficient handling of complex lighting scenarios. Let's examine how to implement this technique using rendergraphs and dynamic rendering through a comprehensive breakdown of the setup process. + +=== Deferred Renderer Setup: G-Buffer Resource Configuration + +First, we establish the G-Buffer (Geometry Buffer) resources that will store intermediate geometry information for the deferred lighting pass. + +[source,cpp] +---- +// Comprehensive deferred renderer setup demonstrating rendergraph resource management +// This implementation shows how to efficiently organize multi-pass rendering workflows +void SetupDeferredRenderer(Rendergraph& graph, uint32_t width, uint32_t height) { + + // Configure position buffer for world-space vertex positions + // High precision format preserves positional accuracy for lighting calculations + graph.AddResource("GBuffer_Position", vk::Format::eR16G16B16A16Sfloat, {width, height}, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, + vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + + // Configure normal buffer for surface orientation data + // High precision normals enable accurate lighting and reflection calculations + graph.AddResource("GBuffer_Normal", vk::Format::eR16G16B16A16Sfloat, {width, height}, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, + vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + + // Configure albedo buffer for surface color information + // Standard 8-bit precision sufficient for color data with gamma encoding + graph.AddResource("GBuffer_Albedo", vk::Format::eR8G8B8A8Unorm, {width, height}, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eInputAttachment, + vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); + + // Configure depth buffer for occlusion and depth testing + // High precision depth enables accurate depth reconstruction in lighting pass + graph.AddResource("Depth", vk::Format::eD32Sfloat, {width, height}, + vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eInputAttachment, + vk::ImageLayout::eUndefined, vk::ImageLayout::eDepthStencilAttachmentOptimal); + + // Configure final color buffer for the completed lighting result + // Standard color format with transfer capability for presentation or post-processing + graph.AddResource("FinalColor", vk::Format::eR8G8B8A8Unorm, {width, height}, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc, + vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal); +---- + +The G-Buffer resource configuration represents the foundation of deferred rendering, where each buffer stores specific geometric information that will be consumed during lighting calculations. The format choices reflect a balance between precision requirements and memory efficiency: positions and normals use 16-bit floating point for accurate lighting calculations, while albedo uses 8-bit integers for color data where gamma correction naturally reduces precision requirements. + +The usage flag combinations enable each resource to serve dual roles: first as render targets during the geometry pass, then as input textures during the lighting pass. This dual usage pattern is characteristic of deferred rendering workflows, where the same data moves through multiple pipeline stages with different access patterns. + +=== Deferred Renderer Setup: Geometry Pass Configuration and Multiple Render Target Setup + +Next, we configure the geometry pass that populates the G-Buffer with geometric information from the scene's 3D models. + +[source,cpp] +---- + // Configure geometry pass for G-Buffer population + // This pass renders all geometry and stores intermediate data for lighting calculations + graph.AddPass("GeometryPass", + {}, // No input dependencies - first pass in pipeline + {"GBuffer_Position", "GBuffer_Normal", "GBuffer_Albedo", "Depth"}, // Outputs all G-Buffer components + [&](vk::raii::CommandBuffer& cmd) { + + // Configure multiple render target attachments for G-Buffer output + // Each attachment corresponds to a different geometric property + std::array colorAttachments; + + // Configure position attachment - world space vertex positions + colorAttachments[0].setImageView(/* GBuffer_Position view */) // Target position buffer + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) // Optimal for writes + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to known state + .setStoreOp(vk::AttachmentStoreOp::eStore); // Preserve for lighting pass + + // Configure normal attachment - surface normals in world space + colorAttachments[1].setImageView(/* GBuffer_Normal view */) // Target normal buffer + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to default normal + .setStoreOp(vk::AttachmentStoreOp::eStore); // Preserve for lighting + + // Configure albedo attachment - surface color and material properties + colorAttachments[2].setImageView(/* GBuffer_Albedo view */) // Target albedo buffer + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to default color + .setStoreOp(vk::AttachmentStoreOp::eStore); // Preserve for lighting + + // Configure depth attachment for occlusion culling + vk::RenderingAttachmentInfoKHR depthAttachment; + depthAttachment.setImageView(/* Depth view */) // Target depth buffer + .setImageLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal) // Optimal for depth ops + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to far plane + .setStoreOp(vk::AttachmentStoreOp::eStore) // Preserve for lighting pass + .setClearValue({1.0f, 0}); // Clear to maximum depth + + // Assemble complete rendering configuration + vk::RenderingInfoKHR renderingInfo; + renderingInfo.setRenderArea({{0, 0}, {width, height}}) // Full screen rendering + .setLayerCount(1) // Single layer rendering + .setColorAttachmentCount(colorAttachments.size()) // Number of G-Buffer targets + .setPColorAttachments(colorAttachments.data()) // G-Buffer attachment array + .setPDepthAttachment(&depthAttachment); // Depth testing configuration + + // Execute geometry rendering with dynamic rendering + cmd.beginRendering(renderingInfo); // Begin G-Buffer population + + // Bind geometry pipeline and render all scene objects + // Each draw call populates position, normal, and albedo for visible fragments + // ... (geometry rendering implementation would go here) + + cmd.endRendering(); // Complete G-Buffer population + }); +---- + +The geometry pass configuration demonstrates the power of deferred rendering's separation of concerns, where geometric complexity is handled independently of lighting complexity. The multiple render target setup enables simultaneous output to all G-Buffer components in a single rendering pass, maximizing GPU efficiency compared to multiple separate passes. + +The dynamic rendering approach eliminates the need to pre-configure render pass objects, providing flexibility to adjust G-Buffer formats or attachment counts based on runtime requirements. This flexibility is particularly valuable for techniques like adaptive quality settings or optional G-Buffer components for different material types. + +=== Deferred Renderer Setup: Lighting Pass Configuration and Screen-Space Processing + +Now we should set up the lighting pass that reads from the G-Buffer and performs all lighting calculations in screen space, producing the final rendered image. + +[source,cpp] +---- + // Configure lighting pass for screen-space illumination calculations + // This pass reads G-Buffer data and computes final lighting for each pixel + graph.AddPass("LightingPass", + {"GBuffer_Position", "GBuffer_Normal", "GBuffer_Albedo", "Depth"}, // Read all G-Buffer components + {"FinalColor"}, // Output final lit result + [&](vk::raii::CommandBuffer& cmd) { + + // Configure single color output for final lighting result + vk::RenderingAttachmentInfoKHR colorAttachment; + colorAttachment.setImageView(/* FinalColor view */) // Target final color buffer + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) // Optimal for color writes + .setLoadOp(vk::AttachmentLoadOp::eClear) // Clear to background color + .setStoreOp(vk::AttachmentStoreOp::eStore) // Preserve final result + .setClearValue({0.0f, 0.0f, 0.0f, 1.0f}); // Clear to black background + + // Configure lighting pass rendering without depth testing + // Depth testing unnecessary since we're processing each pixel exactly once + vk::RenderingInfoKHR renderingInfo; + renderingInfo.setRenderArea({{0, 0}, {width, height}}) // Full screen processing + .setLayerCount(1) // Single layer output + .setColorAttachmentCount(1) // Single color output + .setPColorAttachments(&colorAttachment); // Final color attachment + + // Execute screen-space lighting calculations + cmd.beginRendering(renderingInfo); // Begin lighting pass + + // Bind lighting pipeline and draw full-screen quad + // Fragment shader reads G-Buffer textures and computes lighting for each pixel + // All scene lights are processed in a single screen-space pass + // ... (lighting calculation implementation would go here) + + cmd.endRendering(); // Complete lighting calculations + }); + + // Compile the complete rendergraph for execution + // This analyzes dependencies and generates optimal execution plan + graph.Compile(); +} +---- + +The lighting pass represents the core advantage of deferred rendering: decoupling lighting complexity from geometric complexity. By processing lighting in screen space, the cost becomes proportional to screen resolution rather than scene complexity, enabling efficient handling of scenes with many lights or complex lighting models. + +The single render target configuration reflects the unified nature of the lighting pass, where all lighting contributions are accumulated into the final color buffer. This approach enables advanced lighting techniques like physically-based rendering or global illumination algorithms that would be prohibitively expensive in forward rendering scenarios with complex geometry. + +==== Best Practices for Rendergraphs and Synchronization + +1. *Minimize Synchronization*: Use the rendergraph to minimize the number of synchronization points. +2. *Batch Similar Operations*: Group similar operations together to reduce state changes. +3. *Use Appropriate Access Flags*: Be specific about which access types you need to synchronize. +4. *Avoid Redundant Barriers*: Let the rendergraph eliminate redundant barriers. +5. *Consider Memory Aliasing*: Use the rendergraph's memory aliasing capabilities to reduce memory usage. +6. *Profile and Optimize*: Use GPU profiling tools to identify synchronization bottlenecks. +7. *Handle Platform Differences*: Different GPUs may have different synchronization requirements. + +[source,cpp] +---- +// Forward declarations +class RenderPass; +class RenderTarget; + +// Render pass manager +class RenderPassManager { +private: + std::unordered_map> renderPasses; + std::vector sortedPasses; + bool dirty = true; + +public: + template + T* AddRenderPass(const std::string& name, Args&&... args) { + static_assert(std::is_base_of::value, "T must derive from RenderPass"); + + auto it = renderPasses.find(name); + if (it != renderPasses.end()) { + return dynamic_cast(it->second.get()); + } + + auto pass = std::make_unique(std::forward(args)...); + T* passPtr = pass.get(); + renderPasses[name] = std::move(pass); + dirty = true; + + return passPtr; + } + + RenderPass* GetRenderPass(const std::string& name) { + auto it = renderPasses.find(name); + if (it != renderPasses.end()) { + return it->second.get(); + } + return nullptr; + } + + void RemoveRenderPass(const std::string& name) { + auto it = renderPasses.find(name); + if (it != renderPasses.end()) { + renderPasses.erase(it); + dirty = true; + } + } + + void Execute(vk::raii::CommandBuffer& commandBuffer) { + if (dirty) { + SortPasses(); + dirty = false; + } + + for (auto pass : sortedPasses) { + pass->Execute(commandBuffer); + } + } + +private: + void SortPasses() { + // Topologically sort render passes based on dependencies + sortedPasses.clear(); + + // Create a copy of render passes for sorting + std::unordered_map passMap; + for (const auto& [name, pass] : renderPasses) { + passMap[name] = pass.get(); + } + + // Perform topological sort + std::unordered_set visited; + std::unordered_set visiting; + + for (const auto& [name, pass] : passMap) { + if (visited.find(name) == visited.end()) { + TopologicalSort(name, passMap, visited, visiting); + } + } + } + + void TopologicalSort(const std::string& name, + const std::unordered_map& passMap, + std::unordered_set& visited, + std::unordered_set& visiting) { + visiting.insert(name); + + auto pass = passMap.at(name); + for (const auto& dep : pass->GetDependencies()) { + if (visited.find(dep) == visited.end()) { + if (visiting.find(dep) != visiting.end()) { + // Circular dependency detected + throw std::runtime_error("Circular dependency detected in render passes"); + } + TopologicalSort(dep, passMap, visited, visiting); + } + } + + visiting.erase(name); + visited.insert(name); + sortedPasses.push_back(pass); + } +}; + +// Base render pass class +class RenderPass { +private: + std::string name; + std::vector dependencies; + RenderTarget* target = nullptr; + bool enabled = true; + +public: + explicit RenderPass(const std::string& passName) : name(passName) {} + virtual ~RenderPass() = default; + + const std::string& GetName() const { return name; } + + void AddDependency(const std::string& dependency) { + dependencies.push_back(dependency); + } + + const std::vector& GetDependencies() const { + return dependencies; + } + + void SetRenderTarget(RenderTarget* renderTarget) { + target = renderTarget; + } + + RenderTarget* GetRenderTarget() const { + return target; + } + + void SetEnabled(bool isEnabled) { + enabled = isEnabled; + } + + bool IsEnabled() const { + return enabled; + } + + virtual void Execute(vk::raii::CommandBuffer& commandBuffer) { + if (!enabled) return; + + BeginPass(commandBuffer); + Render(commandBuffer); + EndPass(commandBuffer); + } + +protected: + // With dynamic rendering, BeginPass typically calls vkCmdBeginRendering + // instead of vkCmdBeginRenderPass + virtual void BeginPass(vk::raii::CommandBuffer& commandBuffer) = 0; + virtual void Render(vk::raii::CommandBuffer& commandBuffer) = 0; + // With dynamic rendering, EndPass typically calls vkCmdEndRendering + // instead of vkCmdEndRenderPass + virtual void EndPass(vk::raii::CommandBuffer& commandBuffer) = 0; +}; + +// Render target class +class RenderTarget { +private: + vk::raii::Image colorImage = nullptr; + vk::raii::DeviceMemory colorMemory = nullptr; + vk::raii::ImageView colorImageView = nullptr; + + vk::raii::Image depthImage = nullptr; + vk::raii::DeviceMemory depthMemory = nullptr; + vk::raii::ImageView depthImageView = nullptr; + + uint32_t width; + uint32_t height; + +public: + RenderTarget(uint32_t w, uint32_t h) : width(w), height(h) { + // Create color and depth images + CreateColorResources(); + CreateDepthResources(); + + // Note: With dynamic rendering, we don't need to create VkRenderPass + // or VkFramebuffer objects. Instead, we just create the images and + // image views that will be used directly with vkCmdBeginRendering. + } + + // No need for explicit destructor with RAII objects + + vk::ImageView GetColorImageView() const { return *colorImageView; } + vk::ImageView GetDepthImageView() const { return *depthImageView; } + + uint32_t GetWidth() const { return width; } + uint32_t GetHeight() const { return height; } + +private: + void CreateColorResources() { + // Implementation to create color image, memory, and view + // With dynamic rendering, we just need to create the image and image view + // that will be used with vkCmdBeginRendering + // ... + } + + void CreateDepthResources() { + // Implementation to create depth image, memory, and view + // With dynamic rendering, we just need to create the image and image view + // that will be used with vkCmdBeginRendering + // ... + } + + vk::raii::Device& GetDevice() { + // Get device from somewhere (e.g., singleton or parameter) + // ... + static vk::raii::Device device = nullptr; // Placeholder + return device; + } +}; +---- + +=== Implementing Specific Render Passes + +Now let's implement some specific render passes: + +[source,cpp] +---- +// Geometry pass for deferred rendering +class GeometryPass : public RenderPass { +private: + CullingSystem* cullingSystem; + + // G-buffer textures + RenderTarget* gBuffer; + +public: + GeometryPass(const std::string& name, CullingSystem* culling) + : RenderPass(name), cullingSystem(culling) { + // Create G-buffer render target + gBuffer = new RenderTarget(1920, 1080); // Example resolution + SetRenderTarget(gBuffer); + } + + ~GeometryPass() override { + delete gBuffer; + } + +protected: + void BeginPass(vk::raii::CommandBuffer& commandBuffer) override { + // Begin rendering with dynamic rendering + vk::RenderingInfoKHR renderingInfo; + + // Set up color attachment + vk::RenderingAttachmentInfoKHR colorAttachment; + colorAttachment.setImageView(gBuffer->GetColorImageView()) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})); + + // Set up depth attachment + vk::RenderingAttachmentInfoKHR depthAttachment; + depthAttachment.setImageView(gBuffer->GetDepthImageView()) + .setImageLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(vk::ClearDepthStencilValue(1.0f, 0)); + + // Configure rendering info + renderingInfo.setRenderArea(vk::Rect2D({0, 0}, {gBuffer->GetWidth(), gBuffer->GetHeight()})) + .setLayerCount(1) + .setColorAttachmentCount(1) + .setPColorAttachments(&colorAttachment) + .setPDepthAttachment(&depthAttachment); + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + } + + void Render(vk::raii::CommandBuffer& commandBuffer) override { + // Get visible entities + const auto& visibleEntities = cullingSystem->GetVisibleEntities(); + + // Render each entity to G-buffer + for (auto entity : visibleEntities) { + auto meshComponent = entity->GetComponent(); + auto transformComponent = entity->GetComponent(); + + if (meshComponent && transformComponent) { + // Bind pipeline for G-buffer rendering + // ... + + // Set model matrix + // ... + + // Draw mesh + // ... + } + } + } + + void EndPass(vk::raii::CommandBuffer& commandBuffer) override { + // End dynamic rendering + commandBuffer.endRendering(); + } +}; + +// Lighting pass for deferred rendering +class LightingPass : public RenderPass { +private: + GeometryPass* geometryPass; + std::vector lights; + +public: + LightingPass(const std::string& name, GeometryPass* gPass) + : RenderPass(name), geometryPass(gPass) { + // Add dependency on geometry pass + AddDependency(gPass->GetName()); + } + + void AddLight(Light* light) { + lights.push_back(light); + } + + void RemoveLight(Light* light) { + auto it = std::find(lights.begin(), lights.end(), light); + if (it != lights.end()) { + lights.erase(it); + } + } + +protected: + void BeginPass(vk::raii::CommandBuffer& commandBuffer) override { + // Begin rendering with dynamic rendering + vk::RenderingInfoKHR renderingInfo; + + // Set up color attachment for the lighting pass + vk::RenderingAttachmentInfoKHR colorAttachment; + colorAttachment.setImageView(GetRenderTarget()->GetColorImageView()) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})); + + // Configure rendering info + renderingInfo.setRenderArea(vk::Rect2D({0, 0}, {GetRenderTarget()->GetWidth(), GetRenderTarget()->GetHeight()})) + .setLayerCount(1) + .setColorAttachmentCount(1) + .setPColorAttachments(&colorAttachment); + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + } + + void Render(vk::raii::CommandBuffer& commandBuffer) override { + // Bind G-buffer textures from the geometry pass + auto gBuffer = geometryPass->GetRenderTarget(); + + // Set up descriptor sets for G-buffer textures + // With dynamic rendering, we access the G-buffer textures directly as shader resources + // rather than as subpass inputs + + // Render full-screen quad with lighting shader + // ... + + // For each light + for (auto light : lights) { + // Set light properties + // ... + + // Draw light volume + // ... + } + } + + void EndPass(vk::raii::CommandBuffer& commandBuffer) override { + // End dynamic rendering + commandBuffer.endRendering(); + } +}; + +// Post-process effect base class +class PostProcessEffect { +public: + virtual ~PostProcessEffect() = default; + virtual void Apply(vk::raii::CommandBuffer& commandBuffer) = 0; +}; + +// Post-processing pass +class PostProcessPass : public RenderPass { +private: + LightingPass* lightingPass; + std::vector effects; + +public: + PostProcessPass(const std::string& name, LightingPass* lPass) + : RenderPass(name), lightingPass(lPass) { + // Add dependency on lighting pass + AddDependency(lPass->GetName()); + } + + void AddEffect(PostProcessEffect* effect) { + effects.push_back(effect); + } + + void RemoveEffect(PostProcessEffect* effect) { + auto it = std::find(effects.begin(), effects.end(), effect); + if (it != effects.end()) { + effects.erase(it); + } + } + +protected: + void BeginPass(vk::raii::CommandBuffer& commandBuffer) override { + // Begin rendering with dynamic rendering + vk::RenderingInfoKHR renderingInfo; + + // Set up color attachment for the post-processing pass + vk::RenderingAttachmentInfoKHR colorAttachment; + colorAttachment.setImageView(GetRenderTarget()->GetColorImageView()) + .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + .setClearValue(vk::ClearColorValue(std::array{0.0f, 0.0f, 0.0f, 1.0f})); + + // Configure rendering info + renderingInfo.setRenderArea(vk::Rect2D({0, 0}, {GetRenderTarget()->GetWidth(), GetRenderTarget()->GetHeight()})) + .setLayerCount(1) + .setColorAttachmentCount(1) + .setPColorAttachments(&colorAttachment); + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + } + + void Render(vk::raii::CommandBuffer& commandBuffer) override { + // With dynamic rendering, each effect can set up its own rendering state + // and access input textures directly as shader resources + + // Apply each post-process effect + for (auto effect : effects) { + effect->Apply(commandBuffer); + } + } + + void EndPass(vk::raii::CommandBuffer& commandBuffer) override { + // End dynamic rendering + commandBuffer.endRendering(); + } +}; +---- + +=== Command Generation and Execution + +Once we have our render passes set up, we need to generate and execute commands: + +[source,cpp] +---- +class Renderer { +private: + vk::raii::Device device = nullptr; + vk::Queue graphicsQueue; + vk::raii::CommandPool commandPool = nullptr; + + RenderPassManager renderPassManager; + CullingSystem cullingSystem; + + // Current frame resources + vk::raii::CommandBuffer commandBuffer = nullptr; + vk::raii::Fence fence = nullptr; + vk::raii::Semaphore imageAvailableSemaphore = nullptr; + vk::raii::Semaphore renderFinishedSemaphore = nullptr; + +public: + Renderer(vk::raii::Device& dev, vk::Queue queue) : device(dev), graphicsQueue(queue) { + // Create command pool + // ... + + // Create synchronization objects + // ... + + // Set up render passes + SetupRenderPasses(); + } + + // No need for explicit destructor with RAII objects + + void SetCamera(Camera* camera) { + cullingSystem.SetCamera(camera); + } + + void Render(const std::vector& entities) { + // Wait for previous frame to finish + fence.wait(UINT64_MAX); + fence.reset(); + + // Reset command buffer + commandBuffer.reset(); + + // Perform culling + cullingSystem.CullScene(entities); + + // Record commands + vk::CommandBufferBeginInfo beginInfo; + commandBuffer.begin(beginInfo); + + // Execute render passes + renderPassManager.Execute(commandBuffer); + + commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo; + + // With vk::raii, we need to dereference the command buffer + vk::CommandBuffer rawCommandBuffer = *commandBuffer; + submitInfo.setCommandBufferCount(1); + submitInfo.setPCommandBuffers(&rawCommandBuffer); + + // Set up wait and signal semaphores + vk::PipelineStageFlags waitStages[] = { vk::PipelineStageFlagBits::eColorAttachmentOutput }; + + // With vk::raii, we need to dereference the semaphores + vk::Semaphore rawImageAvailableSemaphore = *imageAvailableSemaphore; + vk::Semaphore rawRenderFinishedSemaphore = *renderFinishedSemaphore; + + submitInfo.setWaitSemaphoreCount(1); + submitInfo.setPWaitSemaphores(&rawImageAvailableSemaphore); + submitInfo.setPWaitDstStageMask(waitStages); + submitInfo.setSignalSemaphoreCount(1); + submitInfo.setPSignalSemaphores(&rawRenderFinishedSemaphore); + + // With vk::raii, we need to dereference the fence + vk::Fence rawFence = *fence; + graphicsQueue.submit(1, &submitInfo, rawFence); + } + +private: + void SetupRenderPasses() { + // Create geometry pass + auto geometryPass = renderPassManager.AddRenderPass("GeometryPass", &cullingSystem); + + // Create lighting pass + auto lightingPass = renderPassManager.AddRenderPass("LightingPass", geometryPass); + + // Create post-process pass + auto postProcessPass = renderPassManager.AddRenderPass("PostProcessPass", lightingPass); + + // Add post-process effects + // ... + } +}; +---- + +=== Advanced Rendering Techniques + +For detailed information about advanced rendering techniques such as Deferred Rendering, Forward+ Rendering, and Physically Based Rendering (PBR), please refer to the link:../Appendix/appendix.adoc#advanced-rendering-techniques[Advanced Rendering Techniques] section in the Appendix. This section includes references to valuable resources for further reading. + +=== Conclusion + +A well-designed rendering pipeline is essential for creating a flexible and efficient rendering engine. By implementing the techniques described in this section, you can create a system that: + +1. Efficiently culls invisible objects +2. Organizes rendering into passes with clear dependencies +3. Supports advanced rendering techniques like deferred rendering and PBR +4. Can be easily extended with new effects and features + +In the next section, we'll explore event systems, which provide a flexible way for different parts of your engine to communicate with each other. + +link:04_resource_management.adoc[Previous: Resource Management] | link:06_event_systems.adoc[Next: Event Systems] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc new file mode 100644 index 00000000..8e6c4ab1 --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/06_event_systems.adoc @@ -0,0 +1,568 @@ +:pp: {plus}{plus} + += Engine Architecture: Event Systems + +== Event Systems + +Event systems provide a flexible way for different parts of your engine to communicate with each other without creating tight coupling. In this section, we'll explore how to design and implement an effective event system for your rendering engine. + +=== The Need for Event Systems + +Even in the simple engine we're building, subsystems need to communicate with each other efficiently. As our engine grows, these communication needs become increasingly important: + +1. *Physics* needs to notify *Audio* when collisions occur. +2. *Input* needs to notify *Game Logic* when buttons are pressed. +3. *Game Logic* needs to notify *Rendering* when objects change. +4. *Resource Management* needs to notify *Rendering* when assets are loaded. + +Without an event system, these interactions would require direct references between subsystems, creating tight coupling and making the code harder to maintain and extend. + +=== Event System Design Principles + +When designing an event system, consider these principles: + +1. *Decoupling* - Minimize dependencies between event producers and consumers. +2. *Type Safety* - Use the type system to prevent errors. +3. *Performance* - Efficiently dispatch events, especially for high-frequency events. +4. *Flexibility* - Support different event delivery patterns (immediate, queued, etc.). +5. *Debugging* - Make it easy to debug event flow. + +=== Basic Event System Implementation + +Let's implement a basic event system: + +==== Base event type and convenience macro + +We start with a minimal base event interface and a helper macro to define strongly typed events without boilerplate. + +[source,cpp] +---- +// Base event class +class Event { +public: + virtual ~Event() = default; + + // Get the type of the event + virtual const char* GetType() const = 0; + + // Clone the event (for queued events) + virtual Event* Clone() const = 0; +}; + +// Macro to help define event types +#define DEFINE_EVENT_TYPE(type) \ + static const char* GetStaticType() { return #type; } \ + virtual const char* GetType() const override { return GetStaticType(); } \ + virtual Event* Clone() const override { return new type(*this); } +---- + +This lets us identify and copy events generically while keeping concrete event classes small. + +==== Concrete event types + +Keep event payloads focused and lightweight; they should represent facts, not behavior. + +[source,cpp] +---- +// Example event types +class WindowResizeEvent : public Event { +private: + int width; + int height; + +public: + WindowResizeEvent(int w, int h) : width(w), height(h) {} + + int GetWidth() const { return width; } + int GetHeight() const { return height; } + + DEFINE_EVENT_TYPE(WindowResizeEvent) +}; + +class KeyPressEvent : public Event { +private: + int keyCode; + bool repeat; + +public: + KeyPressEvent(int key, bool isRepeat) : keyCode(key), repeat(isRepeat) {} + + int GetKeyCode() const { return keyCode; } + bool IsRepeat() const { return repeat; } + + DEFINE_EVENT_TYPE(KeyPressEvent) +}; +---- + +==== Listener and type-safe dispatcher + +Listeners receive events; the dispatcher routes a generic Event to a typed handler when types match. + +[source,cpp] +---- +// Event listener interface +class EventListener { +public: + virtual ~EventListener() = default; + virtual void OnEvent(const Event& event) = 0; +}; + +// Event dispatcher +class EventDispatcher { +private: + const Event& event; + +public: + explicit EventDispatcher(const Event& e) : event(e) {} + + // Dispatch event to handler if types match + template + bool Dispatch(const F& handler) { + if (event.GetType() == T::GetStaticType()) { + handler(static_cast(event)); + return true; + } + return false; + } +}; +---- + +==== Event bus (immediate vs. queued) + +The bus can deliver immediately (low latency) or queue for later (deterministic ordering across frames). + +[source,cpp] +---- +// Event bus +class EventBus { +private: + std::vector listeners; + std::queue> eventQueue; + std::mutex queueMutex; + bool immediateMode = true; + +public: + void SetImmediateMode(bool immediate) { + immediateMode = immediate; + } + + void AddListener(EventListener* listener) { + listeners.push_back(listener); + } + + void RemoveListener(EventListener* listener) { + auto it = std::find(listeners.begin(), listeners.end(), listener); + if (it != listeners.end()) { + listeners.erase(it); + } + } + + void PublishEvent(const Event& event) { + if (immediateMode) { + // Dispatch event immediately + for (auto listener : listeners) { + listener->OnEvent(event); + } + } else { + // Queue event for later processing + std::lock_guard lock(queueMutex); + eventQueue.push(std::unique_ptr(event.Clone())); + } + } + + void ProcessEvents() { + if (immediateMode) return; + + std::queue> currentEvents; + + { + std::lock_guard lock(queueMutex); + std::swap(currentEvents, eventQueue); + } + + while (!currentEvents.empty()) { + auto& event = *currentEvents.front(); + + for (auto listener : listeners) { + listener->OnEvent(event); + } + + currentEvents.pop(); + } + } +}; +---- + +=== Using the Event System + +Here's how you might use the event system in your application: + +[source,cpp] +---- +// Component that listens for events +class CameraController : public Component, public EventListener { +private: + CameraComponent* camera; + float moveSpeed = 5.0f; + float rotateSpeed = 0.1f; + + bool moveForward = false; + bool moveBackward = false; + bool moveLeft = false; + bool moveRight = false; + +public: + void Initialize() override { + camera = GetOwner()->GetComponent(); + + // Register as event listener + GetEventBus().AddListener(this); + } + + void Update(float deltaTime) override { + if (!camera) return; + + // Handle movement + glm::vec3 movement(0.0f); + + if (moveForward) movement.z -= 1.0f; + if (moveBackward) movement.z += 1.0f; + if (moveLeft) movement.x -= 1.0f; + if (moveRight) movement.x += 1.0f; + + if (glm::length(movement) > 0.0f) { + movement = glm::normalize(movement) * moveSpeed * deltaTime; + + auto transform = GetOwner()->GetComponent(); + if (transform) { + glm::vec3 position = transform->GetPosition(); + position += movement; + transform->SetPosition(position); + } + } + } + + void OnEvent(const Event& event) override { + EventDispatcher dispatcher(event); + + // Handle key press events + dispatcher.Dispatch([this](const KeyPressEvent& e) { + switch (e.GetKeyCode()) { + case KEY_W: moveForward = true; break; + case KEY_S: moveBackward = true; break; + case KEY_A: moveLeft = true; break; + case KEY_D: moveRight = true; break; + } + return false; + }); + + // Handle key release events + dispatcher.Dispatch([this](const KeyReleaseEvent& e) { + switch (e.GetKeyCode()) { + case KEY_W: moveForward = false; break; + case KEY_S: moveBackward = false; break; + case KEY_A: moveLeft = false; break; + case KEY_D: moveRight = false; break; + } + return false; + }); + + // Handle window resize events + dispatcher.Dispatch([this](const WindowResizeEvent& e) { + if (camera) { + float aspectRatio = static_cast(e.GetWidth()) / static_cast(e.GetHeight()); + camera->SetAspectRatio(aspectRatio); + } + return false; + }); + } + + ~CameraController() override { + // Unregister as event listener + GetEventBus().RemoveListener(this); + } + +private: + EventBus& GetEventBus() { + // Get event bus from somewhere (e.g., singleton or parameter) + static EventBus eventBus; + return eventBus; + } +}; + +// Input system that generates events +class InputSystem { +private: + EventBus& eventBus; + + // Key states + std::unordered_map keyStates; + +public: + explicit InputSystem(EventBus& bus) : eventBus(bus) {} + + void Update() { + // Poll input events from the platform + // ... + + // Example: Process a key press + ProcessKeyPress(KEY_W, false); + } + + void ProcessKeyPress(int keyCode, bool repeat) { + bool& keyState = keyStates[keyCode]; + + if (!keyState || repeat) { + // Key was not pressed before or this is a repeat + KeyPressEvent event(keyCode, repeat); + eventBus.PublishEvent(event); + } + + keyState = true; + } + + void ProcessKeyRelease(int keyCode) { + bool& keyState = keyStates[keyCode]; + + if (keyState) { + // Key was pressed before + KeyReleaseEvent event(keyCode); + eventBus.PublishEvent(event); + } + + keyState = false; + } +}; +---- + +=== Advanced Event System Features + +==== Event Categories + +Events can be categorized to allow listeners to filter which types of events they receive: + +[source,cpp] +---- +// Event categories +enum class EventCategory { + None = 0, + Application = 1 << 0, + Input = 1 << 1, + Keyboard = 1 << 2, + Mouse = 1 << 3, + MouseButton = 1 << 4, + Window = 1 << 5 +}; + +// Enhanced event base class +class Event { +public: + virtual ~Event() = default; + + virtual const char* GetType() const = 0; + virtual Event* Clone() const = 0; + + // Get the categories this event belongs to + virtual int GetCategoryFlags() const = 0; + + // Check if event is in category + bool IsInCategory(EventCategory category) const { + return GetCategoryFlags() & static_cast(category); + } +}; + +// Enhanced macro to define event types with categories +#define DEFINE_EVENT_TYPE_CATEGORY(type, categoryFlags) \ + static const char* GetStaticType() { return #type; } \ + virtual const char* GetType() const override { return GetStaticType(); } \ + virtual Event* Clone() const override { return new type(*this); } \ + virtual int GetCategoryFlags() const override { return categoryFlags; } + +// Example event with categories +class KeyPressEvent : public Event { +private: + int keyCode; + bool repeat; + +public: + KeyPressEvent(int key, bool isRepeat) : keyCode(key), repeat(isRepeat) {} + + int GetKeyCode() const { return keyCode; } + bool IsRepeat() const { return repeat; } + + DEFINE_EVENT_TYPE_CATEGORY(KeyPressEvent, + static_cast(EventCategory::Input) | + static_cast(EventCategory::Keyboard)) +}; +---- + +==== Event Filtering + +Listeners can filter events based on categories: + +[source,cpp] +---- +// Enhanced event bus with filtering +class EventBus { +private: + struct ListenerInfo { + EventListener* listener; + int categoryFilter; + }; + + std::vector listeners; + std::queue> eventQueue; + std::mutex queueMutex; + bool immediateMode = true; + +public: + void AddListener(EventListener* listener, int categoryFilter = -1) { + listeners.push_back({listener, categoryFilter}); + } + + void RemoveListener(EventListener* listener) { + auto it = std::find_if(listeners.begin(), listeners.end(), + [listener](const ListenerInfo& info) { + return info.listener == listener; + }); + if (it != listeners.end()) { + listeners.erase(it); + } + } + + void PublishEvent(const Event& event) { + if (immediateMode) { + // Dispatch event immediately + for (const auto& info : listeners) { + if (info.categoryFilter == -1 || (event.GetCategoryFlags() & info.categoryFilter)) { + info.listener->OnEvent(event); + } + } + } else { + // Queue event for later processing + std::lock_guard lock(queueMutex); + eventQueue.push(std::unique_ptr(event.Clone())); + } + } + + // Rest of the implementation... +}; +---- + +==== Event Priorities + +Some events may need to be processed before others: + +[source,cpp] +---- +// Enhanced event bus with priorities +class EventBus { +private: + struct ListenerInfo { + EventListener* listener; + int categoryFilter; + int priority; + }; + + std::vector listeners; + // Rest of the implementation... + +public: + void AddListener(EventListener* listener, int categoryFilter = -1, int priority = 0) { + listeners.push_back({listener, categoryFilter, priority}); + + // Sort listeners by priority (higher priority first) + std::sort(listeners.begin(), listeners.end(), + [](const ListenerInfo& a, const ListenerInfo& b) { + return a.priority > b.priority; + }); + } + + // Rest of the implementation... +}; +---- + +==== Event Bubbling and Capturing + +In hierarchical systems like UI, events can propagate through the hierarchy in two ways: + +* *Event Bubbling* - The event starts at the target element and "bubbles up" through parent elements in the hierarchy. For example, a click event on a button first triggers on the button, then on its container, and continues up to the root element. + +* *Event Capturing* - The event starts at the root element and travels down the hierarchy to the target element (the opposite direction of bubbling). + +This approach allows parent elements to intercept and handle events triggered on their children, while also giving children the ability to stop propagation if needed. For hierarchical systems like UI, this provides a flexible way to handle events at the appropriate level: + +[source,cpp] +---- +// UI event with bubbling +class UIEvent : public Event { +private: + UIElement* target; + bool bubbles; + bool cancelBubble = false; + +public: + UIEvent(UIElement* targetElement, bool bubbling = true) + : target(targetElement), bubbles(bubbling) {} + + UIElement* GetTarget() const { return target; } + bool Bubbles() const { return bubbles; } + + void StopPropagation() { + cancelBubble = true; + } + + bool IsPropagationStopped() const { + return cancelBubble; + } + + DEFINE_EVENT_TYPE_CATEGORY(UIEvent, static_cast(EventCategory::UI)) +}; + +// UI system with event bubbling +class UISystem { +public: + void DispatchEvent(UIEvent& event) { + UIElement* target = event.GetTarget(); + + // Capturing phase (top-down) + std::vector path; + UIElement* current = target; + + while (current) { + path.push_back(current); + current = current->GetParent(); + } + + // Dispatch to each element in the path (bottom-up) + for (auto it = path.rbegin(); it != path.rend(); ++it) { + (*it)->OnEvent(event); + + if (event.IsPropagationStopped()) { + break; + } + } + } +}; +---- + +=== Conclusion + +A well-designed event system is crucial for creating a flexible and maintainable engine architecture. By implementing the techniques described in this section, you can create a system that: + +1. Decouples subsystems, making your code more modular and easier to maintain +2. Provides type-safe event handling +3. Supports different event delivery patterns +4. Can be extended with advanced features like filtering, priorities, and bubbling + +This concludes our exploration of engine architecture. In this chapter, we've covered: + +1. Architectural patterns for structuring your engine +2. Component systems for building flexible game objects +3. Resource management for efficiently handling assets +4. Rendering pipeline design for flexible and efficient rendering +5. Event systems for decoupled communication between subsystems + +With these foundations in place, you're well-equipped to build a robust and flexible rendering engine that can be extended to support a wide range of features and techniques. + +link:05_rendering_pipeline.adoc[Previous: Rendering Pipeline] | link:conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc new file mode 100644 index 00000000..f6aa92ea --- /dev/null +++ b/en/Building_a_Simple_Engine/Engine_Architecture/conclusion.adoc @@ -0,0 +1,53 @@ +:pp: {plus}{plus} + += Engine Architecture: Conclusion + +== Conclusion + +In this chapter, we've explored the fundamental architectural patterns and design principles that form the backbone of a modern rendering engine. Let's recap what we've learned and discuss how to apply these concepts in your own engine development. + +=== What We've Covered + +This chapter has taken you through the foundational thinking that separates successful engine development from ad-hoc rendering code. We began by examining architectural patterns that have proven effective in production engines—layered architecture provides clear separation of concerns, component-based systems enable flexible object composition, data-oriented design unlocks performance potential, and service locators manage dependencies cleanly. Understanding these patterns helps you choose the right structural approach for different engine subsystems, balancing flexibility against complexity based on your specific needs. + +The component system implementation demonstrated how composition can replace deep inheritance hierarchies, creating more maintainable and flexible code. This approach allows you to build diverse game objects by combining reusable components rather than creating complex class hierarchies that become difficult to extend and modify. + +Resource management emerged as a critical foundation that affects every other system. Our implementation showcases how reference counting, caching, and hot reloading work together to optimize memory usage while improving development workflow. These techniques become essential as your projects scale beyond simple scenes to complex, asset-heavy applications. + +The rendering pipeline structure provides the framework for accommodating different rendering techniques and effects. By organizing stages for scene culling, render pass management, command generation, and post-processing, we've created a system that can evolve with your rendering needs without requiring fundamental architectural changes. + +Finally, the event system implementation shows how to enable communication between engine subsystems without creating tight coupling. Features like event filtering, priorities, and bubbling create a flexible communication layer that scales from simple notifications to complex interaction patterns. + +=== Applying These Concepts + +The transition from understanding architectural patterns to implementing them successfully requires a disciplined approach that balances ambition with pragmatism. Starting with minimal implementations provides a solid foundation you can build upon—complex architectures often hide subtle bugs that become exponentially harder to debug as system complexity increases. Each additional layer of abstraction should solve a concrete problem you've encountered, not anticipate hypothetical future needs. + +Interface design becomes your most powerful tool for managing complexity as your engine grows. Well-defined interfaces act as contracts between subsystems, allowing you to modify or completely replace implementations without cascading changes throughout your codebase. This separation of concerns proves invaluable when optimizing performance, adding features, or adapting to new requirements. + +Performance considerations need to inform architectural decisions from the beginning, though this differs from premature optimization. Certain structural choices—like data layout patterns, memory allocation strategies, and inter-system communication mechanisms—create performance ceilings that become extremely expensive to change later. Understanding these implications helps you make informed trade-offs during initial design phases. + +Successful engine development requires embracing iteration and refactoring as core activities rather than necessary evils. Your understanding of requirements will evolve as you build and use your engine, and rigid adherence to initial designs often leads to increasingly awkward workarounds. Regular refactoring keeps your architecture aligned with actual needs rather than theoretical ideals. + +The balance between flexibility and complexity represents perhaps the most challenging aspect of engine architecture. Every abstraction layer and configurable system adds cognitive overhead and potential failure points, but insufficient flexibility leads to brittle, hard-to-extend code. Finding the right balance depends on understanding your specific project constraints, team size, timeline, and performance requirements. + +=== Next Steps + +The architectural foundation we've established in this chapter will support everything we build in subsequent chapters. As we progress through camera systems, lighting, model loading, and advanced features, each new system will integrate with these core patterns rather than existing as isolated components. + +Active implementation proves far more valuable than passive reading when learning engine architecture. Build the code examples as you encounter them, but don't stop there—experiment with variations to understand how different approaches affect your engine's behavior. This experimentation develops the intuitive understanding that separates competent engine developers from those who merely copy implementations. + +The architectural concepts we've covered provide a foundation, but production engines require additional sophistication. The xref:../Appendix/appendix.adoc[Appendix] explores advanced rendering techniques and architectural patterns that build on these fundamentals, helping you understand how simple patterns scale to handle complex real-world requirements. + +Studying existing open-source engines like link:https://github.com/TheCherno/Hazel[Hazel] or examining the architectural decisions in established frameworks like link:https://github.com/LWJGL/lwjgl3[LWJGL] provides valuable perspective on how these concepts apply in practice. Look for patterns we've discussed and notice how different engines make different trade-offs based on their specific goals and constraints. + +The graphics programming community offers tremendous value for learning and problem-solving. Engaging with forums, Discord servers, and GitHub discussions exposes you to diverse approaches and helps you get feedback on your architectural decisions. Often, discussing your implementation choices with others reveals assumptions you didn't realize you were making. + +Remember that engine development is an iterative process. Your architecture will evolve as you gain experience and as your requirements change. The concepts we've covered provide a foundation, but the best architecture for your engine will depend on your specific goals and constraints. + +=== Final Thoughts + +Building a rendering engine is a challenging but rewarding endeavor. By applying the architectural patterns and design principles we've explored in this chapter, you'll be well-equipped to create a robust, flexible, and maintainable engine that can grow with your needs. + +Good luck with your engine development journey! + +xref:06_event_systems.adoc[Previous: Event Systems] | xref:../Camera_Transformations/01_introduction.adoc[Next: Camera Transformations] diff --git a/en/Building_a_Simple_Engine/GUI/01_introduction.adoc b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc new file mode 100644 index 00000000..b3b2b67a --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/01_introduction.adoc @@ -0,0 +1,46 @@ +:pp: {plus}{plus} + += GUI: Introduction + +== Introduction + +Welcome to the "GUI" chapter of our "Building a Simple Engine" series! After implementing a camera system in the previous chapter, we'll now focus on adding a graphical user interface (GUI) to our Vulkan application. A well-designed GUI is essential for creating interactive applications that allow users to control settings, display information, and interact with the 3D scene. + +In this chapter, we'll integrate a popular immediate-mode GUI library called Dear ImGui with our Vulkan engine. Dear ImGui is widely used in the game and graphics industry due to its simplicity, performance, and flexibility. It allows developers to quickly create debug interfaces, tools, and in-game menus without the complexity of traditional retained-mode GUI systems. + +This chapter will guide you through integrating a professional GUI system into your Vulkan engine. We'll start by setting up Dear ImGui with Vulkan, establishing the foundation for all GUI functionality. The integration requires careful management of Vulkan resources—we'll create dedicated buffers, textures, and pipelines that work alongside your existing rendering systems without interference. + +User input handling becomes more complex when you need to support both 3D scene navigation and GUI interaction. We'll implement a system that can distinguish between input intended for the 3D world and input meant for interface elements, ensuring smooth interaction with both. + +Rather than overwhelming you with exhaustive widget examples, we'll focus on the key integration concepts that enable GUI functionality. Understanding these principles will let you implement any interface elements your project needs. + +The rendering integration presents interesting challenges—your GUI needs to render on top of your 3D scene without disrupting the existing pipeline. We'll solve this by carefully managing render passes and ensuring proper depth testing and blending. + +Finally, we'll implement object picking, which bridges the gap between your GUI and 3D scene. This feature allows users to click on 3D objects and see their properties in the interface, creating a cohesive development environment. + +By the end of this chapter, you'll have a functional GUI system that you can use to control your camera, adjust rendering settings, and interact with your 3D scene. This will serve as a foundation for more advanced features in later chapters, such as material editors, scene hierarchies, and debugging tools. + +== Prerequisites + +This chapter builds directly on the Camera & Transformations chapter, as we'll extend the camera system we developed there to work seamlessly with GUI interaction. The camera controls need to be aware of when the user is interacting with interface elements versus navigating the 3D scene. + +You'll also need a solid understanding of several core Vulkan concepts. The rendering pipeline and command buffer knowledge is crucial because GUI rendering requires careful coordination with your existing 3D rendering—we'll be recording GUI draw calls into the same command buffers while managing different pipeline states. + +Buffer and image creation skills are essential since Dear ImGui requires dedicated vertex and index buffers for its geometry, plus texture resources for fonts and any custom UI textures. Understanding descriptor sets and layouts becomes important as we'll need to create descriptors specifically for GUI rendering that don't interfere with your 3D scene descriptors. + +Pipeline creation knowledge ties everything together, as we'll build a specialized graphics pipeline for GUI rendering with different vertex input, shaders, and render state than your 3D pipeline. + +A basic understanding of input handling concepts will help you follow along as we implement the dual-mode input system that can distinguish between 3D navigation and GUI interaction. + +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* xref:../../06_Texture_mapping/00_Images.adoc[Texture mapping] + +Let's begin by exploring how to implement a professional GUI system with Dear ImGui and Vulkan. + +link:../Lighting_Materials/06_conclusion.adoc[Previous: Lighting & Materials Conclusion] | link:02_imgui_setup.adoc[Next: Setting Up Dear ImGui] diff --git a/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc new file mode 100644 index 00000000..3a3a7131 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/02_imgui_setup.adoc @@ -0,0 +1,844 @@ +:pp: {plus}{plus} + += GUI: Setting Up Dear ImGui + +== Setting Up Dear ImGui + +In this section, we'll set up Dear ImGui in our Vulkan application. Dear ImGui (also known simply as ImGui) is a bloat-free graphical user interface library for C++. It outputs optimized vertex buffers that you can render with your 3D-pipeline-enabled application. It's particularly well-suited for integration with graphics APIs like Vulkan. + +=== Adding ImGui to Your Project + +First, we need to add ImGui to our project. There are several ways to do this: + +1. *Git Submodule*: Add ImGui as a Git submodule to your project +2. *Package Manager*: Use a package manager like vcpkg or Conan +3. *Manual Integration*: Download and include the ImGui source files directly + +For this tutorial, we'll use the manual integration approach for simplicity: + +[source,bash] +---- +# Clone ImGui repository +git clone https://github.com/ocornut/imgui.git external/imgui + +# Copy necessary files to your project +cp external/imgui/imgui.h include/ +cp external/imgui/imgui.cpp src/ +cp external/imgui/imgui_draw.cpp src/ +cp external/imgui/imgui_widgets.cpp src/ +cp external/imgui/imgui_tables.cpp src/ +cp external/imgui/imgui_demo.cpp src/ +---- + + +Next, update your CMakeLists.txt to include these files: + +[source,cmake] +---- +# ImGui files +set(IMGUI_SOURCES + src/imgui.cpp + src/imgui_draw.cpp + src/imgui_widgets.cpp + src/imgui_tables.cpp + src/imgui_demo.cpp +) + +# Our custom ImGui Vulkan integration +set(IMGUI_VULKAN_SOURCES + src/imgui_vulkan_util.cpp +) + +add_executable(VulkanApp + src/main.cpp + ${IMGUI_SOURCES} + ${IMGUI_VULKAN_SOURCES} +) + +target_include_directories(VulkanApp PRIVATE include) +---- + +=== Creating an ImGui Integration + +Let's implement the ImGuiVulkanUtil class to handle the integration between ImGui and Vulkan. + +The ImGuiVulkanUtil class serves as the bridge between ImGui's immediate-mode GUI system and Vulkan's explicit graphics API. This integration requires careful management of GPU resources, synchronization, and rendering state to efficiently display user interface elements alongside our 3D graphics. Let's break down the class architecture into logical components to understand how each part contributes to the overall integration. + +=== ImGuiVulkanUtil Architecture: GPU Resource Management Foundation + +First, we establish the core Vulkan resources needed to render ImGui's dynamically generated UI geometry on the GPU. + +[source,cpp] +---- +// ImGuiVulkanUtil.h +#pragma once + +#include +#include + +class ImGuiVulkanUtil { +private: + // Core GPU rendering resources for UI display + // These objects form the foundation of our ImGui-to-Vulkan rendering pipeline + vk::raii::Sampler sampler{nullptr}; // Texture sampling configuration for font rendering + Buffer vertexBuffer; // Dynamic vertex buffer for UI geometry + Buffer indexBuffer; // Dynamic index buffer for UI triangle connectivity + uint32_t vertexCount = 0; // Current vertex count for draw commands + uint32_t indexCount = 0; // Current index count for draw commands + Image fontImage; // GPU texture containing ImGui font atlas + ImageView fontImageView; // Shader-accessible view of font texture +---- + +The GPU resource foundation reflects ImGui's dynamic rendering model, where UI geometry is generated fresh each frame based on the current interface layout. The vertex and index buffers use host-visible memory to enable efficient CPU updates, while the font texture remains static once loaded. This hybrid approach balances the need for dynamic UI updates with the performance benefits of GPU-resident font data. + +The buffer sizing strategy must accommodate ImGui's variable geometry output, which can change dramatically based on UI complexity. Unlike static 3D models, ImGui generates different amounts of geometry each frame, requiring our buffers to resize dynamically or be pre-allocated with sufficient capacity for worst-case scenarios. + +=== ImGuiVulkanUtil Architecture: Vulkan Pipeline Infrastructure + +Next, we set up the Vulkan pipeline objects that define how UI geometry is processed and rendered by the GPU. + +[source,cpp] +---- + // Vulkan pipeline infrastructure for UI rendering + // These objects define the complete GPU processing pipeline for ImGui elements + vk::raii::PipelineCache pipelineCache{nullptr}; // Pipeline compilation cache for faster startup + vk::raii::PipelineLayout pipelineLayout{nullptr}; // Resource binding layout (textures, uniforms) + vk::raii::Pipeline pipeline{nullptr}; // Complete graphics pipeline for UI rendering + vk::raii::DescriptorPool descriptorPool{nullptr}; // Pool for allocating descriptor sets + vk::raii::DescriptorSetLayout descriptorSetLayout{nullptr}; // Layout defining shader resource bindings + vk::raii::DescriptorSet descriptorSet{nullptr}; // Actual resource bindings for font texture +---- + +The pipeline infrastructure creates a specialized graphics pipeline optimized for UI rendering, which differs significantly from typical 3D rendering pipelines. UI rendering typically requires alpha blending for transparency effects, operates in 2D screen space rather than 3D world space, and uses simpler shading models focused on texture sampling rather than complex lighting calculations. + +[NOTE] +==== +Frames-in-flight safety: If your renderer uses more than one frame in flight and you do not stall the GPU between frames, you must duplicate the dynamic ImGui buffers (vertex/index) per frame-in-flight. Using a single shared vertex/index buffer risks the CPU overwriting data still in use by the GPU from a previous frame. The simple single-buffer members shown above are for conceptual clarity; in production, store vectors of buffers/memories sized to the max frames in flight and update/bind the buffers for the current frame index. +==== + +The descriptor system manages the connection between our CPU-side resources and the GPU shaders. For UI rendering, this primarily involves binding the font atlas texture to the fragment shader, though more complex UI systems might include additional textures for icons, backgrounds, or other visual elements. + +=== ImGuiVulkanUtil Architecture: Device Context and System Integration + +Then, we maintain references to the Vulkan device context and manage integration with the broader graphics system. + +[source,cpp] +---- + // Vulkan device context and system integration + // These references connect our UI system to the broader Vulkan application context + vk::raii::Device* device = nullptr; // Primary Vulkan device for resource creation + vk::raii::PhysicalDevice* physicalDevice = nullptr; // GPU hardware info for capability queries + vk::raii::Queue* graphicsQueue = nullptr; // Command submission queue for UI rendering + uint32_t graphicsQueueFamily = 0; // Queue family index for validation +---- + +The device context integration demonstrates the explicit nature of Vulkan's resource management, where every operation requires specific device and queue references. Unlike higher-level graphics APIs that maintain global state, Vulkan requires explicit specification of which GPU device and command queue should handle each operation. + +The queue family index enables validation and optimization by ensuring that UI rendering operations use compatible queue types. While UI rendering typically uses the same graphics queue as 3D rendering, some applications might benefit from dedicated queues for different rendering responsibilities. + +=== ImGuiVulkanUtil Architecture: UI State and Rendering Configuration + +After that, we manage UI-specific state including styling, rendering parameters, and dynamic update tracking. + +[source,cpp] +---- + // UI state management and rendering configuration + // These members control the visual appearance and dynamic behavior of the UI system + ImGuiStyle vulkanStyle; // Custom visual styling for Vulkan applications + + // Push constants for efficient per-frame parameter updates + // This structure enables fast updates of transformation and styling data + struct PushConstBlock { + glm::vec2 scale; // UI scaling factors for different screen sizes + glm::vec2 translate; // Translation offset for UI positioning + } pushConstBlock; + + // Dynamic state tracking for performance optimization + bool needsUpdateBuffers = false; // Flag indicating buffer resize requirements + + // Modern Vulkan rendering configuration + vk::PipelineRenderingCreateInfo renderingInfo{}; // Dynamic rendering setup parameters + vk::Format colorFormat = vk::Format::eB8G8R8A8Unorm; // Target framebuffer format +---- + +The styling and configuration management reflects ImGui's flexibility in visual presentation while maintaining compatibility with Vulkan's explicit rendering model. The push constants provide an efficient mechanism for updating per-frame parameters like screen resolution changes or UI scaling factors without requiring descriptor set updates. + +The dynamic state tracking optimizes performance by avoiding unnecessary GPU resource updates when the UI layout remains stable between frames. This optimization becomes particularly important in applications with complex UIs where buffer updates could otherwise impact frame rates. + +=== ImGuiVulkanUtil Architecture: Public Interface and Lifecycle Management + +Finally, we define the external interface that applications use to integrate ImGui rendering into their Vulkan rendering pipeline. + +[source,cpp] +---- +public: + // Lifecycle management for proper resource initialization and cleanup + ImGuiVulkanUtil(vk::raii::Device& device, vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Queue& graphicsQueue, uint32_t graphicsQueueFamily); + ~ImGuiVulkanUtil(); + + // Core functionality methods for ImGui integration + void init(float width, float height); // Initialize ImGui context and configure display + void initResources(); // Create all Vulkan resources for rendering + void setStyle(uint32_t index); // Apply visual styling themes + + // Frame-by-frame rendering operations + bool newFrame(); // Begin new ImGui frame and generate geometry + void updateBuffers(); // Upload updated geometry to GPU buffers + void drawFrame(vk::raii::CommandBuffer& commandBuffer); // Record rendering commands to command buffer + + // Input event handling for interactive UI elements + void handleKey(int key, int scancode, int action, int mods); // Process keyboard input events + bool getWantKeyCapture(); // Query if ImGui wants keyboard focus + void charPressed(uint32_t key); // Handle character input for text widgets +}; +---- + +The public interface design balances ease of integration with performance considerations, separating one-time setup operations from per-frame rendering tasks. The initialization methods handle the expensive resource creation that should happen once during application startup, while the frame-by-frame methods focus on efficient updates and rendering. + +The input handling interface enables proper integration with existing input systems, allowing ImGui to capture relevant events while passing through others to the main application. This cooperative approach ensures that UI elements can respond to user interaction without interfering with 3D scene controls or other input handling. + +=== Implementing the ImGuiVulkanUtil Class + +Now let's implement the methods of our ImGuiVulkanUtil class for the Vulkan implementation. + +==== Constructor and Destructor + +First, let's implement the constructor and destructor: + +[source,cpp] +---- +ImGuiVulkanUtil::ImGuiVulkanUtil(vk::raii::Device& device, vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Queue& graphicsQueue, uint32_t graphicsQueueFamily) + : device(&device), physicalDevice(&physicalDevice), + graphicsQueue(&graphicsQueue), graphicsQueueFamily(graphicsQueueFamily), + // Initialize buffers directly + vertexBuffer(*device, 1, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent), + indexBuffer(*device, 1, + vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent) { + + // Set up dynamic rendering info + renderingInfo.colorAttachmentCount = 1; + vk::Format formats[] = { colorFormat }; + renderingInfo.pColorAttachmentFormats = formats; +} + +ImGuiVulkanUtil::~ImGuiVulkanUtil() { + // Wait for device to finish operations before destroying resources + if (device) { + device->waitIdle(); + } + + // All resources are automatically cleaned up by their destructors + // No manual cleanup needed + + // ImGui context is destroyed separately +} +---- + +==== Initialization + +Next, let's implement the initialization methods: + +[source,cpp] +---- +void ImGuiVulkanUtil::init(float width, float height) { + // Initialize ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + // Configure ImGui + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable keyboard controls + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable docking + + // Set display size + io.DisplaySize = ImVec2(width, height); + io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + + // Set up style + vulkanStyle = ImGui::GetStyle(); + vulkanStyle.Colors[ImGuiCol_TitleBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.6f); + vulkanStyle.Colors[ImGuiCol_TitleBgActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f); + vulkanStyle.Colors[ImGuiCol_MenuBarBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + vulkanStyle.Colors[ImGuiCol_Header] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); + vulkanStyle.Colors[ImGuiCol_CheckMark] = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); + + // Apply default style + setStyle(0); +} + +void ImGuiVulkanUtil::setStyle(uint32_t index) { + ImGuiStyle& style = ImGui::GetStyle(); + + switch (index) { + case 0: + // Custom Vulkan style + style = vulkanStyle; + break; + case 1: + // Classic style + ImGui::StyleColorsClassic(); + break; + case 2: + // Dark style + ImGui::StyleColorsDark(); + break; + case 3: + // Light style + ImGui::StyleColorsLight(); + break; + } +} +---- + +==== Resource Initialization + +Now let's implement the method to initialize all Vulkan resources needed for ImGui rendering. This complex process involves several distinct steps that work together to create the GPU resources required for text and UI rendering. + +=== Resource Initialization: Font Data Extraction and Memory Calculation + +First extract font atlas data from ImGui and calculates the memory requirements for GPU storage. + +[source,cpp] +---- +void ImGuiVulkanUtil::initResources() { + // Extract font atlas data from ImGui's internal font system + // ImGui generates a texture atlas containing all glyphs needed for text rendering + ImGuiIO& io = ImGui::GetIO(); + unsigned char* fontData; // Raw pixel data from font atlas + int texWidth, texHeight; // Dimensions of the generated font atlas + io.Fonts->GetTexDataAsRGBA32(&fontData, &texWidth, &texHeight); + + // Calculate total memory requirements for GPU transfer + // Each pixel contains 4 bytes (RGBA) requiring precise memory allocation + vk::DeviceSize uploadSize = texWidth * texHeight * 4 * sizeof(char); +---- + +The font data extraction represents the bridge between ImGui's CPU-based text rendering system and Vulkan's GPU-based texture pipeline. ImGui automatically generates a font atlas that combines all required character glyphs into a single texture, optimizing GPU memory usage and reducing draw calls during text rendering. The RGBA32 format provides full color and alpha support for anti-aliased text rendering. + +=== Resource Initialization: GPU Image Creation and Memory Allocation + +Next, create the GPU image resources that will store the font texture data in video memory. + +[source,cpp] +---- + // Define image dimensions and create extent structure + // Vulkan requires explicit specification of all image dimensions + vk::Extent3D fontExtent{ + static_cast(texWidth), // Image width in pixels + static_cast(texHeight), // Image height in pixels + 1 // Single layer (not a 3D texture or array) + }; + + // Create optimized GPU image for font texture storage + // This image will be sampled by shaders during UI rendering + fontImage = Image(*device, fontExtent, vk::Format::eR8G8B8A8Unorm, + vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + // Create image view for shader access + // The image view defines how shaders interpret the raw image data + fontImageView = ImageView(*device, fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, + vk::ImageAspectFlagBits::eColor); +---- + +The GPU image creation step establishes the foundation for efficient text rendering by allocating device-local memory that provides optimal access speeds for the GPU. The dual usage flags (eSampled | eTransferDst) enable both data upload operations and shader sampling, while the RGBA8_UNORM format ensures consistent color representation across different GPU architectures. + +=== Resource Initialization — Staging Buffer Creation and Data Transfer + +Next, we create a temporary staging buffer and transfer the font data from CPU memory to GPU memory. + +[source,cpp] +---- + // Create staging buffer for efficient CPU-to-GPU data transfer + // Host-visible memory allows direct CPU access for data upload + Buffer stagingBuffer(*device, uploadSize, vk::BufferUsageFlagBits::eTransferSrc, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + + // Map staging buffer memory and copy font data + // Direct memory mapping provides the fastest path for data transfer + void* data = stagingBuffer.map(); // Map GPU memory to CPU address space + memcpy(data, fontData, uploadSize); // Copy font atlas data to GPU memory + stagingBuffer.unmap(); // Unmap memory to ensure data consistency +---- + +The staging buffer approach represents the most efficient method for transferring large amounts of data from CPU to GPU memory in Vulkan. Host-visible memory enables direct CPU access while host-coherent ensures that CPU writes are immediately visible to the GPU without requiring explicit cache flushes. This intermediate step is necessary because device-local memory (where the final image resides) is typically not directly accessible by the CPU. + +=== Resource Initialization — Image Layout Transitions and Data Upload + +Then, we manage the image layout transitions required for safe data transfer in Vulkan's explicit synchronization model. + +[source,cpp] +---- + // Transition image to optimal layout for data reception + // Vulkan requires explicit layout transitions for optimal performance and correctness + transitionImageLayout(fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, + vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferDstOptimal); + + // Execute the actual buffer-to-image copy operation + // This transfers font data from staging buffer to the final GPU image + copyBufferToImage(stagingBuffer.getHandle(), fontImage.getHandle(), + static_cast(texWidth), static_cast(texHeight)); + + // Transition image to shader-readable layout for rendering + // Final layout optimization enables efficient sampling during UI rendering + transitionImageLayout(fontImage.getHandle(), vk::Format::eR8G8B8A8Unorm, + vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal); +---- + +The layout transition sequence ensures that the GPU memory subsystem can optimize its internal data arrangements for each operation type. The eTransferDstOptimal layout provides the best performance for receiving data uploads, while eShaderReadOnlyOptimal enables efficient texture sampling during rendering. These transitions include automatic memory barriers that synchronize access between different GPU pipeline stages. + +=== Resource Initialization — Texture Sampling Configuration and Descriptor Management + +Finally, we create the sampling configuration and descriptor resources needed for shader access to the font texture. + +[source,cpp] +---- + // Configure texture sampling parameters for optimal text rendering + // These settings directly impact text quality and performance + vk::SamplerCreateInfo samplerInfo{}; + samplerInfo.magFilter = vk::Filter::eLinear; // Smooth scaling when magnified + samplerInfo.minFilter = vk::Filter::eLinear; // Smooth scaling when minified + samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear; // Smooth transitions between mip levels + samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; // Prevent texture wrapping + samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; // Clean edge handling + samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; // 3D consistency + samplerInfo.borderColor = vk::BorderColor::eFloatOpaqueWhite; // White border for clamped areas + + sampler = device->createSampler(samplerInfo); // Create the GPU sampler object + + // Create descriptor pool for shader resource binding + // Descriptors provide the interface between shaders and GPU resources + vk::DescriptorPoolSize poolSize{vk::DescriptorType::eCombinedImageSampler, 1}; + + vk::DescriptorPoolCreateInfo poolInfo{}; + poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; // Allow individual descriptor set freeing + poolInfo.maxSets = 2; // Maximum number of descriptor sets + poolInfo.poolSizeCount = 1; // Number of pool size specifications + poolInfo.pPoolSizes = &poolSize; // Pool size configuration + + descriptorPool = device->createDescriptorPool(poolInfo); // Create descriptor pool + + // Create descriptor set layout defining shader resource interface + // This layout must match the binding declarations in the ImGui shaders + vk::DescriptorSetLayoutBinding binding{}; + binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; // Combined texture and sampler + binding.descriptorCount = 1; // Single texture binding + binding.stageFlags = vk::ShaderStageFlagBits::eFragment; // Used in fragment shader + binding.binding = 0; // Shader binding point 0 + + vk::DescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.bindingCount = 1; // Number of bindings in layout + layoutInfo.pBindings = &binding; // Binding configuration array + + descriptorSetLayout = device->createDescriptorSetLayout(layoutInfo); // Create layout object + + // Allocate descriptor set from pool using the defined layout + // This creates the actual binding that connects GPU resources to shaders + vk::DescriptorSetAllocateInfo allocInfo{}; + allocInfo.descriptorPool = *descriptorPool; // Source pool for allocation + allocInfo.descriptorSetCount = 1; // Number of sets to allocate + vk::DescriptorSetLayout layouts[] = {*descriptorSetLayout}; // Layout template array + allocInfo.pSetLayouts = layouts; // Layout configuration + + descriptorSet = std::move(device->allocateDescriptorSets(allocInfo).front()); // Allocate and store set + + // Update descriptor set with actual font texture and sampler resources + // This final step connects the physical GPU resources to the shader binding points + vk::DescriptorImageInfo imageInfo{}; + imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; // Expected image layout + imageInfo.imageView = fontImageView.getHandle(); // Font texture view + imageInfo.sampler = *sampler; // Texture sampler + + vk::WriteDescriptorSet writeSet{}; + writeSet.dstSet = *descriptorSet; // Target descriptor set + writeSet.descriptorCount = 1; // Number of resources to bind + writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; // Resource type + writeSet.pImageInfo = &imageInfo; // Image resource information + writeSet.dstBinding = 0; // Binding point in shader + + device->updateDescriptorSets(1, &writeSet, 0, nullptr); // Execute the binding update + + // Create pipeline cache + vk::PipelineCacheCreateInfo pipelineCacheInfo{}; + pipelineCache = device->createPipelineCache(pipelineCacheInfo); + + // Create pipeline layout + vk::PushConstantRange pushConstantRange{}; + pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex; + pushConstantRange.offset = 0; + pushConstantRange.size = sizeof(PushConstBlock); + + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{}; + pipelineLayoutInfo.setLayoutCount = 1; + vk::DescriptorSetLayout setLayouts[] = {*descriptorSetLayout}; + pipelineLayoutInfo.pSetLayouts = setLayouts; + pipelineLayoutInfo.pushConstantRangeCount = 1; + pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange; + + pipelineLayout = device->createPipelineLayout(pipelineLayoutInfo); + + // Create the graphics pipeline with dynamic rendering + // ... (shader loading, pipeline state setup, etc.) + + // For brevity, we're omitting the full pipeline creation code here + // In a real implementation, you would: + // 1. Load the vertex and fragment shaders + // 2. Set up all the pipeline state (vertex input, input assembly, rasterization, etc.) + // 3. Include the renderingInfo in the pipeline creation to enable dynamic rendering +} +---- + +==== Frame Management and Rendering + +Finally, let's implement the methods for frame management and rendering: + +[source,cpp] +---- +bool ImGuiVulkanUtil::newFrame() { + // Start a new ImGui frame + ImGui::NewFrame(); + + // Create your UI elements here + // For example: + ImGui::Begin("Vulkan ImGui Demo"); + ImGui::Text("Hello, Vulkan!"); + if (ImGui::Button("Click me!")) { + // Handle button click + } + ImGui::End(); + + // End the frame + ImGui::EndFrame(); + + // Render to generate draw data + ImGui::Render(); + + // Check if buffers need updating + ImDrawData* drawData = ImGui::GetDrawData(); + if (drawData && drawData->CmdListsCount > 0) { + if (drawData->TotalVtxCount > vertexCount || drawData->TotalIdxCount > indexCount) { + needsUpdateBuffers = true; + return true; + } + } + + return false; +} + +void ImGuiVulkanUtil::updateBuffers() { + ImDrawData* drawData = ImGui::GetDrawData(); + if (!drawData || drawData->CmdListsCount == 0) { + return; + } + + // Calculate required buffer sizes + vk::DeviceSize vertexBufferSize = drawData->TotalVtxCount * sizeof(ImDrawVert); + vk::DeviceSize indexBufferSize = drawData->TotalIdxCount * sizeof(ImDrawIdx); + + // Resize buffers if needed + if (drawData->TotalVtxCount > vertexCount) { + // Recreate vertex buffer with new size + vertexBuffer = Buffer(*device, vertexBufferSize, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + vertexCount = drawData->TotalVtxCount; + } + + if (drawData->TotalIdxCount > indexCount) { + // Recreate index buffer with new size + indexBuffer = Buffer(*device, indexBufferSize, + vk::BufferUsageFlagBits::eIndexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent); + indexCount = drawData->TotalIdxCount; + } + + // Upload data to buffers + ImDrawVert* vtxDst = static_cast(vertexBuffer.map()); + ImDrawIdx* idxDst = static_cast(indexBuffer.map()); + + 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; + } + + vertexBuffer.unmap(); + indexBuffer.unmap(); +} + +==== Begin a rendering scope + +Before issuing any UI draw commands, we open a dynamic rendering scope that targets the current framebuffer. This replaces vkCmdBeginRenderPass/EndRenderPass and keeps the UI pass lightweight. + +[source,cpp] +---- +void ImGuiVulkanUtil::drawFrame(vk::raii::CommandBuffer& commandBuffer) { + ImDrawData* drawData = ImGui::GetDrawData(); + if (!drawData || drawData->CmdListsCount == 0) { + return; + } + + // Begin dynamic rendering + vk::RenderingAttachmentInfo colorAttachment{}; + // Note: In a real implementation, you would set imageView, imageLayout, + // loadOp, storeOp, and clearValue based on your swapchain image + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, {static_cast(drawData->DisplaySize.x), + static_cast(drawData->DisplaySize.y)}}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = 1; + renderingInfo.pColorAttachments = &colorAttachment; + + commandBuffer.beginRendering(renderingInfo); +---- + +At this point, commands affect the UI overlay only. Next we bind state that doesn’t change per draw. + +==== Bind pipeline and set viewport + +[source,cpp] +---- + // Bind the pipeline used for ImGui + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pipeline); + + // Configure viewport for UI pixel coordinates + vk::Viewport viewport{}; + viewport.width = drawData->DisplaySize.x; + viewport.height = drawData->DisplaySize.y; + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + commandBuffer.setViewport(0, viewport); +---- + +The pipeline has blending and raster states tailored for UI. The viewport maps ImGui’s coordinate system to the framebuffer. + +==== Push per-frame constants + +[source,cpp] +---- + // Convert from ImGui coordinates into NDC via a simple scale/translate + pushConstBlock.scale = glm::vec2(2.0f / drawData->DisplaySize.x, 2.0f / drawData->DisplaySize.y); + pushConstBlock.translate = glm::vec2(-1.0f); + commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eVertex, + 0, sizeof(PushConstBlock), &pushConstBlock); +---- + +This keeps the shader simple and avoids per-vertex work for coordinate transforms. + +==== Bind geometry buffers + +[source,cpp] +---- + // We already filled these buffers this frame + vk::Buffer vertexBuffers[] = { vertexBuffer.getHandle() }; + vk::DeviceSize offsets[] = { 0 }; + commandBuffer.bindVertexBuffers(0, 1, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(indexBuffer.getHandle(), 0, vk::IndexType::eUint16); +---- + +==== Iterate command lists, set scissor, draw + +[source,cpp] +---- + 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]; + + // Clip per draw call + 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 font (and any UI) textures for this draw + commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *pipelineLayout, 0, *descriptorSet, {}); + + // Issue indexed draw for this UI batch + commandBuffer.drawIndexed(pcmd->ElemCount, 1, indexOffset, vertexOffset, 0); + indexOffset += pcmd->ElemCount; + } + + vertexOffset += cmdList->VtxBuffer.Size; + } +---- + +Each ImDrawCmd provides a scissor rect that clips widgets efficiently without extra passes. + +==== End the rendering scope + +[source,cpp] +---- + // Close the rendering scope for the UI overlay + commandBuffer.endRendering(); +} +---- + +=== Input Handling + +Let's implement the input handling methods: + +[source,cpp] +---- +void ImGuiVulkanUtil::handleKey(int key, int scancode, int action, int mods) { + ImGuiIO& io = ImGui::GetIO(); + + // This example uses GLFW key codes and actions, but you can adapt this + // to work with any windowing library's input system + + // Map the platform-specific key action to ImGui's key state + // In GLFW: GLFW_PRESS = 1, GLFW_RELEASE = 0 + const int KEY_PRESSED = 1; // Generic key pressed value + const int KEY_RELEASED = 0; // Generic key released value + + if (action == KEY_PRESSED) + io.KeysDown[key] = true; + if (action == KEY_RELEASED) + io.KeysDown[key] = false; + + // Update modifier keys + // These key codes are GLFW-specific, but you would use your windowing library's + // equivalent key codes for other libraries + const int KEY_LEFT_CTRL = 341; // GLFW_KEY_LEFT_CONTROL + const int KEY_RIGHT_CTRL = 345; // GLFW_KEY_RIGHT_CONTROL + const int KEY_LEFT_SHIFT = 340; // GLFW_KEY_LEFT_SHIFT + const int KEY_RIGHT_SHIFT = 344; // GLFW_KEY_RIGHT_SHIFT + const int KEY_LEFT_ALT = 342; // GLFW_KEY_LEFT_ALT + const int KEY_RIGHT_ALT = 346; // GLFW_KEY_RIGHT_ALT + const int KEY_LEFT_SUPER = 343; // GLFW_KEY_LEFT_SUPER + const int KEY_RIGHT_SUPER = 347; // GLFW_KEY_RIGHT_SUPER + + io.KeyCtrl = io.KeysDown[KEY_LEFT_CTRL] || io.KeysDown[KEY_RIGHT_CTRL]; + io.KeyShift = io.KeysDown[KEY_LEFT_SHIFT] || io.KeysDown[KEY_RIGHT_SHIFT]; + io.KeyAlt = io.KeysDown[KEY_LEFT_ALT] || io.KeysDown[KEY_RIGHT_ALT]; + io.KeySuper = io.KeysDown[KEY_LEFT_SUPER] || io.KeysDown[KEY_RIGHT_SUPER]; +} + +bool ImGuiVulkanUtil::getWantKeyCapture() { + return ImGui::GetIO().WantCaptureKeyboard; +} + +void ImGuiVulkanUtil::charPressed(uint32_t key) { + ImGuiIO& io = ImGui::GetIO(); + io.AddInputCharacter(key); +} +---- + +=== Using the ImGuiVulkanUtil Class + +Now that we've implemented our ImGuiVulkanUtil class, let's see how to use it in a Vulkan application: + +[source,cpp] +---- +// In your application class +ImGuiVulkanUtil imGui; + +// During initialization +void initImGui() { + // Initialize ImGui directly + imGui = ImGuiVulkanUtil( + device, + physicalDevice, + graphicsQueue, + graphicsQueueFamily + ); + + imGui.init(swapChainExtent.width, swapChainExtent.height); + imGui.initResources(); // No renderPass needed with dynamic rendering +} + +// In your render loop +void drawFrame() { + // ... existing frame preparation code ... + + // Update ImGui + if (imGui.newFrame()) { + imGui.updateBuffers(); + } + + // Begin command buffer recording + // Note: With dynamic rendering, we don't need to begin a render pass + // The ImGui drawFrame method will handle dynamic rendering internally + + // Render scene using dynamic rendering + // ... + + // Render ImGui (in multi-frame renderers, pass the current frame index to bind per-frame buffers) + imGui.drawFrame(commandBuffer); + + // ... submit command buffer ... +} + +// Input handling +// This example shows how to handle input with GLFW, but you can adapt this +// to work with any windowing library's input system + +// Example key callback function for GLFW +void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { + // First check if ImGui wants to capture this input + imGui.handleKey(key, scancode, action, mods); + + // If ImGui doesn't want to capture the keyboard, process for your application + if (!imGui.getWantKeyCapture()) { + // Process key for your application + } +} + +// Example character input callback for GLFW +void charCallback(GLFWwindow* window, unsigned int codepoint) { + imGui.charPressed(codepoint); +} + +// With other windowing libraries, you would implement similar callback functions +// using their equivalent APIs and event systems + +// Cleanup +void cleanup() { + // ... existing cleanup code ... + + // ImGui will be automatically cleaned up when the application exits + // No manual cleanup needed +} +---- + +=== Testing the Integration + +To verify that our ImGui integration is working correctly, we can use the ImGui demo window, which showcases all of ImGui's features: + +[source,cpp] +---- +// In your ImGuiVulkanUtil::newFrame method +bool ImGuiVulkanUtil::newFrame() { + ImGui::NewFrame(); + + // Show the demo window + ImGui::ShowDemoWindow(); + + ImGui::EndFrame(); + ImGui::Render(); + + // Check if buffers need updating + // ... +} +---- + +With this implementation, you have a Vulkan implementation for ImGui that allows you to customize the rendering process to fit your specific needs. + +In the next section, we'll explore how to handle input for both the GUI and the 3D scene. + +link:01_introduction.adoc[Previous: Introduction] | link:03_input_handling.adoc[Next: Input Handling] diff --git a/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc new file mode 100644 index 00000000..b28760cc --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/03_input_handling.adoc @@ -0,0 +1,501 @@ +:pp: {plus}{plus} + += GUI: Input Handling + +== Input Handling + +One of the challenges when integrating a GUI into a 3D application is managing input events. We need to ensure that input events are correctly routed to either the GUI or the 3D scene. For example, if the user is interacting with a UI element, we don't want their mouse movements to also rotate the camera. + +In this section, we'll explore how to handle input for both the GUI and the 3D scene, ensuring a smooth user experience regardless of the windowing library you choose to use. + +[NOTE] +==== +A *windowing library* is a software framework that provides functionality for creating and managing application windows, handling user input events (keyboard, mouse, touch), and interfacing with the operating system's display and input systems. Examples include GLFW, SDL, Qt, and SFML. These libraries abstract the platform-specific details of window management and input handling, allowing developers to write code that works across different operating systems without dealing with platform-specific APIs directly. +==== + +=== Creating a Platform-Agnostic Input System + +To create an effective input system that works with any windowing library, we need to abstract the input mechanisms and provide a clean interface. Let's define a simple input system that can be adapted to different platforms: + +[source,cpp] +---- +// InputSystem.h +#pragma once + +#include +#include +#include +#include + +// Input actions that our application can respond to +enum class InputAction { + MOVE_FORWARD, + MOVE_BACKWARD, + MOVE_LEFT, + MOVE_RIGHT, + MOVE_UP, + MOVE_DOWN, + LOOK_UP, + LOOK_DOWN, + LOOK_LEFT, + LOOK_RIGHT, + ZOOM_IN, + ZOOM_OUT, + TOGGLE_UI_MODE, + // Add more actions as needed +}; + +// Input state that tracks the current state of inputs +struct InputState { + glm::vec2 cursorPosition = {0.0f, 0.0f}; + glm::vec2 cursorDelta = {0.0f, 0.0f}; + bool mouseButtons[3] = {false, false, false}; + float scrollDelta = 0.0f; + + // For touch input + struct TouchPoint { + int id; + glm::vec2 position; + glm::vec2 delta; + }; + std::vector touchPoints; + + // Reset delta values after each frame + void resetDeltas() { + cursorDelta = {0.0f, 0.0f}; + scrollDelta = 0.0f; + for (auto& touch : touchPoints) { + touch.delta = {0.0f, 0.0f}; + } + } +}; + +class InputSystem { +public: + static void Initialize(); + static void Shutdown(); + + // Update input state (called once per frame) + static void Update(float deltaTime); + + // Register a callback for an input action + static void RegisterActionCallback(InputAction action, std::function callback); + + // Process a platform-specific input event + static bool ProcessInputEvent(void* event); + + // Get the current input state + static const InputState& GetInputState(); + + // Check if ImGui is capturing input + static bool IsImGuiCapturingKeyboard(); + static bool IsImGuiCapturingMouse(); + +private: + static InputState inputState; + static std::unordered_map> actionCallbacks; +}; +---- + +=== Input Prioritization + +The general approach for input handling in applications with both 3D navigation and GUI is: + +1. First, check if the GUI is capturing input (e.g., mouse is over a UI element) +2. If the GUI is not capturing input, then process the input for 3D navigation + +Let's implement this approach using our cross-platform input system: + +[source,cpp] +---- +void processInput(float deltaTime) { + // Check if ImGui is capturing keyboard input + bool imguiCapturingKeyboard = InputSystem::IsImGuiCapturingKeyboard(); + + // Check if ImGui is capturing mouse input + bool imguiCapturingMouse = InputSystem::IsImGuiCapturingMouse(); + + // Get the current input state + const InputState& inputState = InputSystem::GetInputState(); + + // Process keyboard input for camera movement if ImGui is not capturing keyboard + if (!imguiCapturingKeyboard) { + // Forward these to the camera system + // This could be done through the action callback system + if (InputSystem::IsActionActive(InputAction::MOVE_FORWARD)) + camera.processKeyboard(CameraMovement::FORWARD, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_BACKWARD)) + camera.processKeyboard(CameraMovement::BACKWARD, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_LEFT)) + camera.processKeyboard(CameraMovement::LEFT, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_RIGHT)) + camera.processKeyboard(CameraMovement::RIGHT, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_UP)) + camera.processKeyboard(CameraMovement::UP, deltaTime); + if (InputSystem::IsActionActive(InputAction::MOVE_DOWN)) + camera.processKeyboard(CameraMovement::DOWN, deltaTime); + } + + // Process mouse/touch input for camera rotation if ImGui is not capturing mouse + if (!imguiCapturingMouse) { + if (inputState.cursorDelta.x != 0.0f || inputState.cursorDelta.y != 0.0f) { + camera.processMouseMovement(inputState.cursorDelta.x, -inputState.cursorDelta.y); + } + + if (inputState.scrollDelta != 0.0f) { + camera.processMouseScroll(inputState.scrollDelta); + } + } +} +---- + +=== Implementing Platform Adapters for Input + +While our input system design is platform-agnostic, we still need platform-specific adapters to bridge between our unified interface and each windowing library's native input events. Here's an example implementation using GLFW, a popular windowing library: + +==== Example: GLFW Implementation + +[source,cpp] +---- +// InputSystem_GLFW.cpp + +#include "InputSystem.h" +#include +#include + +// Store the GLFW window pointer +static GLFWwindow* gWindow = nullptr; +static bool mouseCaptureMode = false; + +// GLFW callback functions +static void glfwMouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { + if (button >= 0 && button < 3) { + InputState& state = InputSystem::GetInputState(); + state.mouseButtons[button] = action == GLFW_PRESS; + } +} + +static void glfwCursorPosCallback(GLFWwindow* window, double xpos, double ypos) { + InputState& state = InputSystem::GetInputState(); + + // Calculate delta from last position + glm::vec2 newPos(static_cast(xpos), static_cast(ypos)); + state.cursorDelta = newPos - state.cursorPosition; + state.cursorPosition = newPos; +} + +static void glfwScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { + InputState& state = InputSystem::GetInputState(); + state.scrollDelta = static_cast(yoffset); +} + +static void glfwKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { + // Map GLFW keys to our input actions + if (action == GLFW_PRESS || action == GLFW_RELEASE) { + bool pressed = (action == GLFW_PRESS); + + // Toggle mouse capture mode with Escape key + if (key == GLFW_KEY_ESCAPE && pressed) { + mouseCaptureMode = !mouseCaptureMode; + + if (mouseCaptureMode) { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } else { + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + } + + // Map other keys to actions + // ... + } +} + +void InputSystem::Initialize(GLFWwindow* window) { + gWindow = window; + + // Set up GLFW callbacks + glfwSetMouseButtonCallback(window, glfwMouseButtonCallback); + glfwSetCursorPosCallback(window, glfwCursorPosCallback); + glfwSetScrollCallback(window, glfwScrollCallback); + glfwSetKeyCallback(window, glfwKeyCallback); + + // Initially capture the cursor for camera control + mouseCaptureMode = true; + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); +} + +void InputSystem::Update(float deltaTime) { + // Poll for input events + glfwPollEvents(); + + // Update key states for continuous actions (like movement) + if (glfwGetKey(gWindow, GLFW_KEY_W) == GLFW_PRESS) { + if (auto it = actionCallbacks.find(InputAction::MOVE_FORWARD); it != actionCallbacks.end()) { + it->second(deltaTime); + } + } + + // ... other keys ... + + // Reset delta values after processing + inputState.resetDeltas(); +} + +bool InputSystem::IsImGuiCapturingKeyboard() { + return ImGui::GetIO().WantCaptureKeyboard; +} + +bool InputSystem::IsImGuiCapturingMouse() { + return ImGui::GetIO().WantCaptureMouse; +} + +---- + + +=== Input Modes + +For applications that need different input modes (e.g., camera control vs. UI interaction), we can implement a mode system: + +[source,cpp] +---- +// Define input modes +enum class InputMode { + CAMERA_CONTROL, + UI_INTERACTION, + OBJECT_MANIPULATION +}; + +// Current input mode +static InputMode currentInputMode = InputMode::CAMERA_CONTROL; + +// Set the input mode +void setInputMode(InputMode mode) { + currentInputMode = mode; + + // Update platform-specific settings based on the mode + // This example shows how to implement this with GLFW + if (mode == InputMode::CAMERA_CONTROL) { + // In GLFW, we can disable the cursor for camera control + glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + } else { + // For UI interaction, we want the cursor to be visible + glfwSetInputMode(gWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } + + // With other windowing libraries, you would use their equivalent APIs +} + +// Toggle between camera control and UI interaction modes +void toggleInputMode() { + if (currentInputMode == InputMode::CAMERA_CONTROL) { + setInputMode(InputMode::UI_INTERACTION); + } else { + setInputMode(InputMode::CAMERA_CONTROL); + } +} +---- + +=== Handling GUI-Specific Input + +Some GUI interactions might require special handling. For example, you might want to implement drag-and-drop functionality or custom keyboard shortcuts for UI elements: + +[source,cpp] +---- +void drawGUI() { + // Start a new ImGui frame + ImGui::NewFrame(); + + // Create a window for camera controls + ImGui::Begin("Camera Controls"); + + // Add a button to reset camera position + if (ImGui::Button("Reset Camera")) { + camera.setPosition(glm::vec3(0.0f, 0.0f, 3.0f)); + camera.setYaw(-90.0f); + camera.setPitch(0.0f); + } + + // Add sliders for camera settings + float movementSpeed = camera.getMovementSpeed(); + if (ImGui::SliderFloat("Movement Speed", &movementSpeed, 1.0f, 10.0f)) { + camera.setMovementSpeed(movementSpeed); + } + + float sensitivity = camera.getMouseSensitivity(); + if (ImGui::SliderFloat("Mouse Sensitivity", &sensitivity, 0.1f, 1.0f)) { + camera.setMouseSensitivity(sensitivity); + } + + float zoom = camera.getZoom(); + if (ImGui::SliderFloat("Zoom", &zoom, 1.0f, 45.0f)) { + camera.setZoom(zoom); + } + + ImGui::End(); + + // Render ImGui + ImGui::Render(); +} +---- + +=== Integrating Input Handling with the Main Loop + +Finally, let's integrate our input handling system with the main loop: + +[source,cpp] +---- +void mainLoop() { + // Main application loop + while (isRunning) { + // Calculate delta time + float deltaTime = calculateDeltaTime(); + + // Update input system + InputSystem::Update(deltaTime); + + // Process input for camera and other systems + processInput(deltaTime); + + // Draw GUI + drawGUI(); + + // Update uniform buffer with latest camera data + updateUniformBuffer(currentFrame); + + // Draw frame + drawFrame(); + } +} +---- + +=== Main Loop Integration + +The input system needs to be integrated with your application's main loop. Here's an example of how to do this with GLFW, but similar principles apply to other windowing libraries: + +[source,cpp] +---- +// Example main loop with GLFW +void runMainLoop() { + // Initialize input system with your window + // With GLFW, this would look like: + InputSystem::Initialize(window); + + // Main loop - with GLFW, we check if the window should close + // Other libraries would have their own condition + while (!glfwWindowShouldClose(window)) { + float deltaTime = calculateDeltaTime(); + + // Update input and process events + // This would be platform-specific + InputSystem::Update(deltaTime); + + // Rest of the main loop is platform-independent + processInput(deltaTime); + drawGUI(); + updateUniformBuffer(currentFrame); + drawFrame(); + } +} +---- + + +=== Advanced Input Handling Techniques + +For more complex applications, you might want to consider these advanced input handling techniques: + +==== Gesture Recognition + +Gesture recognition can enhance the user experience regardless of which windowing library you use: + +[source,cpp] +---- +// GestureRecognizer.h +#pragma once + +#include +#include +#include + +enum class GestureType { + TAP, + DOUBLE_TAP, + LONG_PRESS, + SWIPE, + PINCH, + ROTATE, + PAN +}; + +struct GestureEvent { + GestureType type; + glm::vec2 position; + glm::vec2 delta; + float scale; // For pinch + float rotation; // For rotate + int pointerCount; +}; + +class GestureRecognizer { +public: + static void Initialize(); + static void Update(const InputState& inputState, float deltaTime); + + // Register callbacks for different gesture types + static void RegisterGestureCallback(GestureType type, std::function callback); + +private: + static void detectTap(const InputState& inputState); + static void detectSwipe(const InputState& inputState); + static void detectPinch(const InputState& inputState); + static void detectRotate(const InputState& inputState); + static void detectPan(const InputState& inputState); + + static std::unordered_map> gestureCallbacks; +}; +---- + + +==== Input Context System + +For more complex applications with different input requirements in different states: + +[source,cpp] +---- +// InputContext.h +#pragma once + +#include +#include +#include +#include + +class InputContext { +public: + // Create a new input context + static void CreateContext(const std::string& name); + + // Push a context onto the stack (making it active) + static void PushContext(const std::string& name); + + // Pop the top context from the stack + static void PopContext(); + + // Get the current active context + static std::string GetActiveContext(); + + // Register an action handler for a specific context + static void RegisterActionHandler(const std::string& contextName, InputAction action, std::function handler); + + // Process an action in the current context + static void ProcessAction(InputAction action, float deltaTime); + +private: + static std::unordered_map>> contextHandlers; + static std::stack contextStack; +}; +---- + + +With these advanced input handling techniques, your application can provide a consistent and intuitive user experience. In the next section, we'll explore how to create various UI elements to control your application. + +link:02_imgui_setup.adoc[Previous: Setting Up Dear ImGui] | link:04_ui_elements.adoc[Next: UI Elements] diff --git a/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc new file mode 100644 index 00000000..ad4b9688 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/04_ui_elements.adoc @@ -0,0 +1,671 @@ +:pp: {plus}{plus} + += GUI: UI Elements and Integration Concepts + +== UI Elements and Integration Concepts + +Now that we have set up ImGui and implemented input handling, let's explore the key concepts of integrating a GUI with your Vulkan application. We'll focus on the integration aspects rather than exhaustive ImGui widget examples, as those are well-documented in the ImGui documentation. + +=== GUI Integration Concepts + +When integrating a GUI into a 3D application, there are several important concepts to consider: + +1. *Separation of Concerns*: Keep your GUI code separate from your rendering code to maintain clean architecture. +2. *Performance Impact*: GUIs can impact performance, especially with complex layouts or frequent updates. +3. *Input Management*: Properly handle input to ensure it's routed to either the GUI or the 3D scene. +4. *Rendering Order*: The GUI is typically rendered after the 3D scene, as an overlay. +5. *State Management*: Use the GUI to modify application state in a controlled manner. + +=== Basic ImGui Usage + +ImGui follows an immediate-mode paradigm, where the UI is recreated every frame. Here's a simple example: + +[source,cpp] +---- +void drawGUI() { + // Start a new ImGui frame + ImGui::NewFrame(); + + // Create a window + ImGui::Begin("Settings"); + + // Add UI elements here + static bool enableFeature = false; + if (ImGui::Checkbox("Enable Feature", &enableFeature)) { + // This code runs when the checkbox value changes + updateFeatureState(enableFeature); + } + + static float value = 0.5f; + if (ImGui::SliderFloat("Parameter", &value, 0.0f, 1.0f)) { + // This code runs when the slider value changes + updateParameter(value); + } + + ImGui::End(); + + // Render ImGui + ImGui::Render(); +} +---- + +For a comprehensive guide to all available ImGui widgets and their options, please refer to the official ImGui documentation and demo: +https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp + +=== GUI Design Considerations for Vulkan Applications + +When designing a GUI for your Vulkan application, consider these aspects: + +==== Memory Management + +ImGui generates vertex and index buffers that need to be uploaded to the GPU. Ensure these resources are properly managed: + +1. *Buffer Sizing*: Allocate buffers with sufficient size or implement resizing logic +2. *Memory Types*: Use host-visible memory for frequent updates +3. *Synchronization*: Ensure buffer updates are synchronized with rendering + +==== Command Buffer Integration + +Integrate ImGui rendering commands with your Vulkan command buffers: + +[source,cpp] +---- +// Record commands for scene rendering +// ... + +// Record ImGui rendering commands +imGuiUtil.drawFrame(commandBuffer); + +// Submit command buffer +// ... +---- + +==== Descriptor Resources + +ImGui requires descriptors for its font texture. Ensure your descriptor pool has sufficient capacity: + +[source,cpp] +---- +// Create descriptor pool with enough capacity for ImGui +vk::DescriptorPoolSize poolSizes[] = { + { vk::DescriptorType::eCombinedImageSampler, 50 }, + // Other descriptor types... +}; + +vk::DescriptorPoolCreateInfo poolInfo{}; +poolInfo.flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet; +poolInfo.maxSets = 50; +poolInfo.poolSizeCount = static_cast(std::size(poolSizes)); +poolInfo.pPoolSizes = poolSizes; + +descriptorPool = device.createDescriptorPool(poolInfo); +---- + +=== Performance Considerations + +When integrating ImGui with Vulkan, consider these performance aspects: + +1. *Command Buffer Recording*: Record ImGui commands efficiently, ideally once per frame +2. *Descriptor Management*: Minimize descriptor set allocations and updates +3. *Buffer Updates*: Optimize vertex and index buffer updates +4. *Pipeline State*: Use a dedicated pipeline for ImGui to minimize state changes +5. *Render Pass Integration*: Consider whether to use a separate render pass or subpass for the GUI + +==== Frames-in-Flight: Duplicate Dynamic Buffers Per Frame + +If your renderer uses multiple frames in flight (e.g., double/triple buffering) without a device wait-idle between frames, ImGui's dynamic vertex and index buffers must not be shared across frames. Otherwise, the CPU can overwrite data that the GPU from a previous frame is still reading. + +- Allocate one vertex buffer and one index buffer per frame-in-flight. +- Update/bind the buffers for the current frame index only. +- Size each buffer to the frame's ImDrawData TotalVtxCount/TotalIdxCount, growing as needed. + +Example sketch: + +[source,cpp] +---- +class ImGuiSystem { + // ... + std::vector vertexBuffers; + std::vector vertexMemories; + std::vector indexBuffers; + std::vector indexMemories; + std::vector vertexCounts; + std::vector indexCounts; + + bool Initialize(Renderer* renderer, uint32_t w, uint32_t h) { + // ... create pipelines, font, descriptors ... + const uint32_t frames = renderer->GetMaxFramesInFlight(); + vertexBuffers.resize(frames); + vertexMemories.resize(frames); + indexBuffers.resize(frames); + indexMemories.resize(frames); + vertexCounts.assign(frames, 0); + indexCounts.assign(frames, 0); + return true; + } + + void Render(vk::raii::CommandBuffer& cmd, uint32_t frameIndex) { + ImGui::Render(); + updateBuffers(frameIndex); + // bind per-frame buffers + std::array vb = {*vertexBuffers[frameIndex]}; + std::array offs{}; + cmd.bindVertexBuffers(0, vb, offs); + cmd.bindIndexBuffer(*indexBuffers[frameIndex], 0, vk::IndexType::eUint16); + // draw lists... + } + + void updateBuffers(uint32_t frameIndex) { + ImDrawData* dd = ImGui::GetDrawData(); + if (!dd || dd->CmdListsCount == 0) return; + vk::DeviceSize vbytes = dd->TotalVtxCount * sizeof(ImDrawVert); + vk::DeviceSize ibytes = dd->TotalIdxCount * sizeof(ImDrawIdx); + // grow-per-frame if needed, then map/copy for this frame only + // ... + } +}; +---- + +When integrating with your main renderer, pass the current frame index to the ImGui render call: + +[source,cpp] +---- +// inside your frame loop after scene rendering +imguiSystem->Render(commandBuffers[currentFrame], currentFrame); +---- + +=== Organizing Your GUI Code + +For maintainable GUI code, consider these organizational patterns: + +1. *Component-Based Approach*: Split your GUI into logical components +2. *State Management*: Use a centralized state store that the GUI can modify +3. *Event System*: Implement an event system for GUI-triggered actions +4. *Lazy Updates*: Only update Vulkan resources when GUI settings actually change + +[source,cpp] +---- +// Component-based approach example +class VulkanGUI { +private: + // GUI state + struct { + bool showRenderSettings = true; + bool showPerformance = true; + bool showSceneControls = true; + } state; + + // Components + void drawRenderSettingsPanel(); + void drawPerformancePanel(); + void drawSceneControlsPanel(); + +public: + void draw() { + // Start a new ImGui frame + ImGui::NewFrame(); + + // Draw components based on state + if (state.showRenderSettings) drawRenderSettingsPanel(); + if (state.showPerformance) drawPerformancePanel(); + if (state.showSceneControls) drawSceneControlsPanel(); + + // Main menu for toggling panels + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("View")) { + ImGui::MenuItem("Render Settings", nullptr, &state.showRenderSettings); + ImGui::MenuItem("Performance", nullptr, &state.showPerformance); + ImGui::MenuItem("Scene Controls", nullptr, &state.showSceneControls); + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + } + + // Render ImGui + ImGui::Render(); + } +}; +---- + +=== Displaying Textures in ImGui + +A common requirement in GUI systems is displaying textures, such as rendered scenes, material previews, or icons. ImGui provides the ability to display textures through its `ImGui::Image` and `ImGui::ImageButton` functions. To use these with Vulkan, you need to properly set up descriptor sets for your textures. + +==== Setting Up Texture Descriptors + +To display a Vulkan texture in ImGui, you need to: + +1. Create a descriptor set layout for the texture +2. Allocate a descriptor set +3. Update the descriptor set with your texture's image view and sampler +4. Pass the descriptor set handle to ImGui + +===== Create the descriptor set layout +This layout declares a single combined image sampler the shader can sample from when ImGui draws the quad. + +[source,cpp] +---- +// Create a descriptor set layout for textures +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; + +vk::raii::DescriptorSetLayout textureSetLayout = device.createDescriptorSetLayout(layoutInfo); +---- + +===== Allocate a descriptor set +Allocate one set per texture you want to show in ImGui. + +[source,cpp] +---- +// Allocate a descriptor set for each texture +vk::DescriptorSetAllocateInfo allocInfo{}; +allocInfo.descriptorPool = *descriptorPool; +allocInfo.descriptorSetCount = 1; +vk::DescriptorSetLayout layouts[] = {*textureSetLayout}; +allocInfo.pSetLayouts = layouts; + +vk::raii::DescriptorSet textureDescriptorSet = std::move(device.allocateDescriptorSets(allocInfo).front()); +---- + +===== Update the descriptor set +Point the descriptor at your image view and sampler in shader‑read layout. + +[source,cpp] +---- +// Update the descriptor set with your texture +vk::DescriptorImageInfo imageInfo{}; +imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; +imageInfo.imageView = textureImageView.getHandle(); +imageInfo.sampler = *textureSampler; + +vk::WriteDescriptorSet writeSet{}; +writeSet.dstSet = *textureDescriptorSet; +writeSet.descriptorCount = 1; +writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; +writeSet.pImageInfo = &imageInfo; +writeSet.dstBinding = 0; + +device.updateDescriptorSets(1, &writeSet, 0, nullptr); +---- + +==== Use it in ImGui + +Once you have set up the descriptor set, you can use it with ImGui's image functions: + +[source,cpp] +---- +// Store the descriptor set as ImTextureID (which is just a void*) +ImTextureID textureId = (ImTextureID)(VkDescriptorSet)*textureDescriptorSet; + +// Display the texture in ImGui +ImGui::Begin("Texture Viewer"); + +// Display as a simple image +ImGui::Image(textureId, ImVec2(width, height)); + +// Or as an image button +if (ImGui::ImageButton(textureId, ImVec2(width, height))) { + // Handle button click +} + +// You can also apply tinting and modify UV coordinates +ImGui::Image(textureId, ImVec2(width, height), + ImVec2(0, 0), ImVec2(1, 1), // UV coordinates (0,0) to (1,1) for the full texture + ImVec4(1, 1, 1, 1), // Tint color (white = no tint) + ImVec4(1, 1, 1, 0.5)); // Border color + +ImGui::End(); +---- + +==== Complete Example: Texture Manager for ImGui + +Here's a more complete example of a texture manager class that handles multiple textures for ImGui: + +[source,cpp] +---- +class ImGuiTextureManager { +private: + vk::raii::Device* device = nullptr; + vk::raii::DescriptorPool* descriptorPool = nullptr; + vk::raii::DescriptorSetLayout descriptorSetLayout{nullptr}; + + struct TextureInfo { + vk::raii::DescriptorSet descriptorSet{nullptr}; + uint32_t width; + uint32_t height; + }; + + std::unordered_map textures; + +public: + ImGuiTextureManager(vk::raii::Device& device, vk::raii::DescriptorPool& descriptorPool) + : device(&device), descriptorPool(&descriptorPool) { + + // Create descriptor set layout for textures + 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; + + descriptorSetLayout = device.createDescriptorSetLayout(layoutInfo); + } + + // Register a texture for use with ImGui + ImTextureID registerTexture(const std::string& name, vk::ImageView imageView, + vk::Sampler sampler, uint32_t width, uint32_t height) { + + // Allocate descriptor set + vk::DescriptorSetAllocateInfo allocInfo{}; + allocInfo.descriptorPool = **descriptorPool; + allocInfo.descriptorSetCount = 1; + vk::DescriptorSetLayout layouts[] = {*descriptorSetLayout}; + allocInfo.pSetLayouts = layouts; + + vk::raii::DescriptorSet descriptorSet = std::move(device->allocateDescriptorSets(allocInfo).front()); + + // Update descriptor set + vk::DescriptorImageInfo imageInfo{}; + imageInfo.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + imageInfo.imageView = imageView; + imageInfo.sampler = sampler; + + vk::WriteDescriptorSet writeSet{}; + writeSet.dstSet = *descriptorSet; + writeSet.descriptorCount = 1; + writeSet.descriptorType = vk::DescriptorType::eCombinedImageSampler; + writeSet.pImageInfo = &imageInfo; + writeSet.dstBinding = 0; + + device->updateDescriptorSets(1, &writeSet, 0, nullptr); + + // Store texture info + textures[name] = {std::move(descriptorSet), width, height}; + + // Return the descriptor set as ImTextureID + return (ImTextureID)(VkDescriptorSet)*textures[name].descriptorSet; + } + + // Get a previously registered texture + ImTextureID getTexture(const std::string& name) { + if (textures.find(name) == textures.end()) { + throw std::runtime_error("Texture not found: " + name); + } + + return (ImTextureID)(VkDescriptorSet)*textures[name].descriptorSet; + } + + // Get texture dimensions + ImVec2 getTextureDimensions(const std::string& name) { + if (textures.find(name) == textures.end()) { + throw std::runtime_error("Texture not found: " + name); + } + + return ImVec2(static_cast(textures[name].width), + static_cast(textures[name].height)); + } +}; +---- + +==== Usage Example + +Here's how you might use the texture manager in your application: + +[source,cpp] +---- +// During initialization +ImGuiTextureManager textureManager(device, descriptorPool); + +// Register textures (e.g., after loading a model or rendering to a texture) +ImTextureID albedoTexId = textureManager.registerTexture( + "albedo", + albedoImageView, + textureSampler, + albedoWidth, + albedoHeight +); + +ImTextureID normalMapId = textureManager.registerTexture( + "normalMap", + normalMapImageView, + textureSampler, + normalMapWidth, + normalMapHeight +); + +// In your GUI rendering code +void drawMaterialEditor() { + ImGui::Begin("Material Editor"); + + // Display textures + ImGui::Text("Albedo Texture:"); + ImGui::Image(textureManager.getTexture("albedo"), + ImVec2(200, 200)); + + ImGui::Text("Normal Map:"); + ImGui::Image(textureManager.getTexture("normalMap"), + ImVec2(200, 200)); + + // Material properties + static float roughness = 0.5f; + if (ImGui::SliderFloat("Roughness", &roughness, 0.0f, 1.0f)) { + updateMaterialProperty("roughness", roughness); + } + + static float metallic = 0.0f; + if (ImGui::SliderFloat("Metallic", &metallic, 0.0f, 1.0f)) { + updateMaterialProperty("metallic", metallic); + } + + ImGui::End(); +} +---- + +==== Performance Considerations + +When working with textures in ImGui, keep these performance considerations in mind: + +1. *Descriptor Management*: Create descriptor sets for textures only when needed and reuse them +2. *Texture Size*: Consider using smaller preview versions of textures for the UI +3. *Mipmap Selection*: For large textures, ensure proper mipmap selection to avoid aliasing +4. *Texture Updates*: If a texture changes frequently, use a staging buffer for updates +5. *Texture Atlas*: For many small textures (like icons), consider using a texture atlas + +By properly managing textures in your ImGui integration, you can create rich interfaces that display rendered content, material previews, and other visual elements directly in your GUI. + +=== Object Picking: Interacting with the 3D Scene + +An important aspect of GUI integration is handling object picking - selecting 3D objects with the mouse. This requires coordination between ImGui and your 3D scene: + +[source,cpp] +---- +void handleMouseInput(float mouseX, float mouseY) { + // First, check if ImGui is using this input + ImGuiIO& io = ImGui::GetIO(); + if (io.WantCaptureMouse) { + // ImGui is using the mouse, don't use it for 3D picking + return; + } + + // ImGui isn't using the mouse, so we can use it for 3D picking + pickObject(mouseX, mouseY); +} + +void pickObject(float mouseX, float mouseY) { + // Convert screen coordinates to normalized device coordinates + float ndcX = (2.0f * mouseX) / windowWidth - 1.0f; + float ndcY = 1.0f - (2.0f * mouseY) / windowHeight; + + // Create a ray from the camera through the mouse position + glm::vec4 clipCoords(ndcX, ndcY, -1.0f, 1.0f); + glm::vec4 eyeCoords = glm::inverse(projectionMatrix) * clipCoords; + eyeCoords = glm::vec4(eyeCoords.x, eyeCoords.y, -1.0f, 0.0f); + + glm::vec3 rayDirection = glm::normalize(glm::vec3( + glm::inverse(viewMatrix) * eyeCoords + )); + + glm::vec3 rayOrigin = camera.getPosition(); + + // Test for intersections with scene objects + float closestHit = std::numeric_limits::max(); + int hitObjectId = -1; + + for (size_t i = 0; i < sceneObjects.size(); i++) { + float hitDistance; + if (rayIntersectsObject(rayOrigin, rayDirection, sceneObjects[i], hitDistance)) { + if (hitDistance < closestHit) { + closestHit = hitDistance; + hitObjectId = static_cast(i); + } + } + } + + // If we hit an object, select it + if (hitObjectId >= 0) { + selectObject(hitObjectId); + } +} +---- + +==== Implementing Ray-Object Intersection + +For object picking to work, you need to implement ray-object intersection tests. Here's a simple example for sphere intersection: + +[source,cpp] +---- +bool rayIntersectsSphere( + const glm::vec3& rayOrigin, + const glm::vec3& rayDirection, + const glm::vec3& sphereCenter, + float sphereRadius, + float& outDistance +) { + glm::vec3 oc = rayOrigin - sphereCenter; + float a = glm::dot(rayDirection, rayDirection); + float b = 2.0f * glm::dot(oc, rayDirection); + float c = glm::dot(oc, oc) - sphereRadius * sphereRadius; + float discriminant = b * b - 4 * a * c; + + if (discriminant < 0) { + return false; // No intersection + } + + // Calculate the closest intersection point + float t = (-b - sqrt(discriminant)) / (2.0f * a); + if (t < 0) { + // Try the other intersection point + t = (-b + sqrt(discriminant)) / (2.0f * a); + if (t < 0) { + return false; // Both intersection points are behind the ray + } + } + + outDistance = t; + return true; +} +---- + +==== Visualizing Selected Objects + +Once an object is selected, you can visualize the selection: + +[source,cpp] +---- +void drawScene(vk::raii::CommandBuffer& commandBuffer) { + // Draw all objects + for (size_t i = 0; i < sceneObjects.size(); i++) { + // If this object is selected, use a different pipeline + if (static_cast(i) == selectedObjectId) { + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *highlightPipeline); + } else { + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *standardPipeline); + } + + // Draw the object + drawObject(commandBuffer, sceneObjects[i]); + } +} +---- + +==== Integrating Picking with ImGui + +You can also display information about the selected object in the GUI: + +[source,cpp] +---- +void drawObjectPropertiesPanel() { + if (selectedObjectId < 0) { + return; // No object selected + } + + ImGui::Begin("Object Properties"); + + SceneObject& obj = sceneObjects[selectedObjectId]; + + // Display object properties + ImGui::Text("Object ID: %d", selectedObjectId); + ImGui::Text("Name: %s", obj.name.c_str()); + + // Edit object properties + glm::vec3 position = obj.position; + if (ImGui::DragFloat3("Position", &position[0], 0.1f)) { + obj.position = position; + updateObjectTransform(selectedObjectId); + } + + glm::vec3 rotation = obj.rotation; + if (ImGui::DragFloat3("Rotation", &rotation[0], 1.0f, -180.0f, 180.0f)) { + obj.rotation = rotation; + updateObjectTransform(selectedObjectId); + } + + glm::vec3 scale = obj.scale; + if (ImGui::DragFloat3("Scale", &scale[0], 0.1f, 0.1f, 10.0f)) { + obj.scale = scale; + updateObjectTransform(selectedObjectId); + } + + ImGui::End(); +} +---- + +Object picking creates a powerful interaction model where users can select and manipulate 3D objects directly, while using the GUI to fine-tune properties. This combination of direct manipulation and precise control provides an intuitive user experience. + +=== Balancing GUI and 3D Interaction + +When designing your application, consider how to balance GUI-based controls with direct 3D interaction: + +1. *Use GUI for*: + - Precise numerical inputs + - Complex settings with many options + - Hierarchical data visualization + - Application-wide controls + +2. *Use 3D Interaction for*: + - Object placement and movement + - Camera navigation + - Direct manipulation of scene elements + - Intuitive spatial operations + +3. *Hybrid Approaches*: + - Gizmos for 3D transformation with precise control + - Context menus that appear near selected objects + - Property panels that update based on selection + +By thoughtfully integrating ImGui with your Vulkan application and implementing object picking, you can create a powerful and intuitive user interface that combines the strengths of both 2D GUI controls and direct 3D interaction. + +In the next section, we'll explore more details about integrating the GUI rendering with the Vulkan rendering pipeline. + +link:03_input_handling.adoc[Previous: Input Handling] | link:05_vulkan_integration.adoc[Next: Vulkan Integration] diff --git a/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc b/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc new file mode 100644 index 00000000..6260f4b1 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/05_vulkan_integration.adoc @@ -0,0 +1,1001 @@ +:pp: {plus}{plus} + += GUI: Vulkan Integration + +== Vulkan Integration + +In this section, we'll explore how to properly integrate ImGui rendering with the Vulkan rendering pipeline. While we've already covered the basic setup in the "Setting Up Dear ImGui" section, here we'll dive deeper into the technical details of how ImGui works with Vulkan and how to optimize the integration. + +=== Understanding the Rendering Flow + +Before we dive into the implementation details, let's understand how ImGui rendering fits into the Vulkan rendering pipeline: + +1. *Prepare Frame*: Begin a new frame in ImGui and create UI elements +2. *Generate Draw Data*: ImGui generates vertex and index buffers for the UI +3. *Record Commands*: Record Vulkan commands to render the ImGui draw data +4. *Submit Commands*: Submit the commands to the Vulkan queue +5. *Present*: Present the rendered frame to the screen + +This flow needs to be integrated with your existing Vulkan rendering pipeline, which typically involves: + +1. Acquiring the next swap chain image +2. Recording command buffers for scene rendering +3. Submitting command buffers +4. Presenting the rendered image + +=== Dynamic Rendering Configuration + +ImGui can be integrated with Vulkan's dynamic rendering feature, which simplifies the rendering process by eliminating the need for explicit render passes and framebuffers: + +[source,cpp] +---- +// When initializing ImGui, we set up our custom Vulkan renderer with dynamic rendering +ImGuiVulkanRenderer renderer; +// ... configure the renderer ... +renderer.initialize(*device, *physicalDevice); + +// Set up dynamic rendering info +vk::PipelineRenderingCreateInfo renderingInfo{}; +renderingInfo.colorAttachmentCount = 1; +vk::Format formats[] = { vk::Format::eB8G8R8A8Unorm }; +renderingInfo.pColorAttachmentFormats = formats; +renderer.setDynamicRenderingInfo(renderingInfo); +---- + +Dynamic rendering simplifies the integration by removing the dependency on render passes and framebuffers, making the code more flexible and easier to maintain. + +=== Command Buffer Integration + +There are two main approaches to integrating ImGui commands with your Vulkan command buffers: + +1. *Single Command Buffer*: Record both scene and ImGui rendering commands in the same command buffer +2. *Multiple Command Buffers*: Use separate command buffers for scene and ImGui rendering + +Let's look at both approaches: + +==== Single Command Buffer Approach + +This is the simplest approach and works well for most applications. With dynamic rendering, the code becomes even cleaner: + +=== Command Buffer Initialization + +The frame rendering process begins with command buffer preparation, where we set up the recording state and prepare for GPU command submission. + +[source,cpp] +---- +void drawFrame() { + // ... existing frame preparation code ... + + // Initialize command buffer recording + // This tells Vulkan we're about to record a sequence of GPU commands + vk::CommandBufferBeginInfo beginInfo{}; + commandBuffer.begin(beginInfo); +---- + +Command buffer recording represents the heart of Vulkan's explicit GPU control model. Unlike older APIs where rendering commands are immediately submitted to the GPU, Vulkan allows us to build up a complete sequence of operations before submission. This approach enables powerful optimizations like command reordering, parallel command buffer construction, and efficient GPU scheduling. + +The 'begin' operation transitions the command buffer from an initial state into a recording state, where subsequent API calls will be captured as GPU instructions rather than executed immediately. This explicit state management gives us precise control over when and how GPU work is submitted, enabling the fine-grained performance control that makes Vulkan so powerful for demanding applications. + +=== Dynamic Rendering Attachment Setup + +Dynamic rendering requires us to explicitly describe our render targets and their properties, replacing the traditional render pass system with a more flexible approach. + +[source,cpp] +---- + // Configure color attachment for the main render target + // This describes how the GPU should handle the color output + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = *swapChainImageViews[imageIndex]; // Target swapchain image + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; // Optimal layout for color output + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; // Clear the image before rendering + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; // Preserve results after rendering + colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; // Clear to black + + // Configure depth attachment for 3D depth testing + // This enables proper occlusion and depth sorting for 3D objects + vk::RenderingAttachmentInfo depthAttachment{}; + depthAttachment.imageView = *depthImageView; // Depth buffer image + depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; // Optimal for depth operations + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; // Clear depth buffer to far plane + depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; // Don't preserve depth after rendering + depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; // Clear to maximum depth +---- + +The attachment configuration system provides explicit control over how the GPU handles our render targets throughout the rendering process. By specifying load and store operations, we can optimize memory bandwidth by only preserving data that needs to carry forward to subsequent passes. The clear operations ensure we start with a known state, preventing visual artifacts from previous frame data. + +Image layout transitions happen automatically based on our specifications, with the GPU driver handling the necessary memory barriers and cache flushes to ensure data coherency. The optimal layouts we specify here tell the driver to arrange the image data in whatever format provides the best performance for the intended usage, rather than forcing a specific memory organization. + +=== Dynamic Rendering Pass Setup + +With our attachments configured, we now assemble them into a complete rendering pass that describes the full rendering operation to the GPU. + +[source,cpp] +---- + // Assemble the complete rendering operation description + // This ties together all our attachments and rendering parameters + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; // Render to entire swapchain area + renderingInfo.layerCount = 1; // Single layer (not array rendering) + renderingInfo.colorAttachmentCount = 1; // One color output + renderingInfo.pColorAttachments = &colorAttachment; // Our configured color attachment + renderingInfo.pDepthAttachment = &depthAttachment; // Our configured depth attachment + + // Begin the dynamic rendering pass + // This establishes the rendering context for subsequent draw commands + commandBuffer.beginRendering(renderingInfo); +---- + +Dynamic rendering represents a significant evolution from traditional Vulkan render passes, providing greater flexibility while maintaining the performance benefits of explicit GPU control. Instead of pre-defining render pass objects at initialization time, we can specify render targets and their properties at command recording time, enabling more dynamic and flexible rendering architectures. + +The render area specification allows for partial-screen rendering, which can provide significant performance benefits when only portions of the screen need updating. For full-screen rendering like our case, we specify the entire swapchain extent to ensure complete coverage. + +=== 3D Scene Rendering + +The main scene rendering phase handles all 3D geometry, lighting, and material rendering within the established rendering context. + +[source,cpp] +---- + // Execute 3D scene rendering + // All your existing 3D geometry, lighting, and material rendering happens here + // ... your existing scene rendering code ... + + // Complete the 3D rendering pass + // This finalizes all 3D rendering operations and prepares for UI overlay + commandBuffer.endRendering(); +---- + +The scene rendering phase operates within the rendering context we established, with the GPU automatically handling depth testing, color blending, and other rasterization operations according to our pipeline configurations. All draw commands issued between beginRendering and endRendering will target our configured attachments with the specified clear and store behaviors. + +The explicit endRendering call ensures that all scene rendering operations are properly completed and that render targets are transitioned to appropriate states for subsequent operations. This explicit control allows the GPU driver to perform optimal scheduling and memory management for the rendering workload. + +=== UI Overlay Integration + +The final rendering phase integrates ImGui UI elements as an overlay on top of the 3D scene, requiring careful coordination between the two rendering systems. + +[source,cpp] +---- + // Render ImGui UI overlay on top of the 3D scene + // The custom renderer handles ImGui's own dynamic rendering setup internally + // This includes vertex buffer uploads, pipeline binding, and draw command generation + renderer.render(ImGui::GetDrawData(), commandBuffer); + + // Finalize command buffer recording + // This transitions the command buffer to executable state for GPU submission + commandBuffer.end(); + + // Submit command buffer + // ... your existing submission code ... +} +---- + +==== Multiple Command Buffers Approach + +This approach gives you more flexibility and can be useful for more complex rendering pipelines. With dynamic rendering, it becomes even more straightforward: + +=== Multi-Buffer: Scene Command Buffer Recording + +The multiple command buffer approach begins by isolating 3D scene rendering into its own dedicated command buffer, providing greater flexibility for complex rendering pipelines. + +[source,cpp] +---- +void drawFrame() { + // ... existing frame preparation code ... + + // Initialize scene-specific command buffer recording + // This dedicated buffer will contain only 3D geometry and lighting operations + vk::CommandBufferBeginInfo beginInfo{}; + sceneCommandBuffer.begin(beginInfo); +---- + +Separating scene rendering into its own command buffer provides several architectural advantages. First, it enables parallel command buffer recording where different threads can simultaneously build scene and UI command sequences, improving CPU utilization on multi-core systems. Second, it allows for independent optimization of each rendering phase, where scene rendering can use different GPU queues or submission timing than UI rendering. + +This separation also facilitates advanced rendering techniques like multi-frame latency optimization, where scene rendering can be decoupled from UI updates to maintain consistent frame timing even when one system experiences performance variations. + +=== Multi-Buffer: Scene Attachment Configuration + +The scene rendering setup mirrors the single-buffer approach but with explicit ownership of the attachment configuration within the scene command buffer. + +[source,cpp] +---- + // Configure scene rendering attachments with explicit ownership + // These configurations belong specifically to the scene rendering pass + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = *swapChainImageViews[imageIndex]; // Target swapchain image + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; // Optimal for color rendering + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; // Clear for fresh scene start + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; // Preserve for UI overlay + colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; // Clear to black + + // Configure depth attachment for 3D scene depth testing + // UI rendering won't need depth testing, so this is scene-specific + vk::RenderingAttachmentInfo depthAttachment{}; + depthAttachment.imageView = *depthImageView; // Scene depth buffer + depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; // Optimal for depth ops + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; // Clear depth for new frame + depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; // UI doesn't need depth data + depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; // Clear to far plane +---- + +The attachment configuration for scene rendering emphasizes the separation of concerns between 3D and UI rendering. The store operation for the color attachment ensures that scene rendering results are preserved for the subsequent UI overlay, while the depth attachment uses "don't care" storage since UI elements typically render without depth testing. + +This explicit configuration makes the rendering dependencies clear and helps optimize memory bandwidth by only preserving the data that subsequent passes actually need. + +=== Multi-Buffer: Scene Rendering Execution + +The scene rendering execution occurs within its dedicated command buffer, providing isolated control over 3D rendering operations. + +[source,cpp] +---- + // Assemble scene rendering configuration + // This defines the complete 3D rendering context + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; // Full screen rendering + renderingInfo.layerCount = 1; // Single rendering layer + renderingInfo.colorAttachmentCount = 1; // One color output + renderingInfo.pColorAttachments = &colorAttachment; // Scene color configuration + renderingInfo.pDepthAttachment = &depthAttachment; // Scene depth configuration + + // Execute complete 3D scene rendering pass + sceneCommandBuffer.beginRendering(renderingInfo); + // All 3D geometry, lighting, materials, and effects render here + // ... your existing scene rendering code ... + sceneCommandBuffer.endRendering(); + + // Finalize scene command buffer for submission + sceneCommandBuffer.end(); +---- + +The scene rendering execution benefits from having its own isolated command buffer context, where all GPU state changes and draw calls are contained within a clearly defined scope. This isolation makes debugging easier, as scene-specific rendering issues can be analyzed independently of UI rendering complexity. + +Command buffer finalization with `end()` transitions the buffer to an executable state, ready for GPU submission, while maintaining clear boundaries between different rendering responsibilities. + +=== Multi-Buffer: UI Command Buffer Setup + +The UI rendering phase begins with its own command buffer recording, configured specifically for overlay rendering requirements. + +[source,cpp] +---- + // Initialize UI-specific command buffer recording + // This dedicated buffer handles only UI overlay operations + imguiCommandBuffer.begin(beginInfo); + + // Configure UI attachment to preserve scene rendering results + // This is the key difference from scene rendering - we load existing content + colorAttachment.loadOp = vk::AttachmentLoadOp::eLoad; // Preserve scene rendering + + // Ensure proper ordering/visibility between scene and UI when using multiple command buffers. + // If you submit scene and UI command buffers separately, synchronize them either by: + // - Submitting both on the same queue with a semaphore (scene signals, UI waits with stage = COLOR_ATTACHMENT_OUTPUT), or + // - Recording a pipeline barrier in the UI command buffer before beginRendering() to make scene color writes visible. + // Example barrier inserted in the UI command buffer: + { + vk::ImageMemoryBarrier2 barrier{ + .srcStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .srcAccessMask = vk::AccessFlagBits2::eColorAttachmentWrite, + .dstStageMask = vk::PipelineStageFlagBits2::eColorAttachmentOutput, + .dstAccessMask = vk::AccessFlagBits2::eColorAttachmentRead | vk::AccessFlagBits2::eColorAttachmentWrite, + .oldLayout = vk::ImageLayout::eColorAttachmentOptimal, + .newLayout = vk::ImageLayout::eColorAttachmentOptimal, + .image = *swapChainImages[imageIndex], + .subresourceRange = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 } + }; + vk::DependencyInfo depInfo{ .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &barrier }; + imguiCommandBuffer.pipelineBarrier2(depInfo); + } + + // UI rendering typically doesn't need depth testing + // Remove depth attachment to optimize UI rendering performance + renderingInfo.pDepthAttachment = nullptr; +---- + +The UI command buffer setup demonstrates the power of the multi-buffer approach through its different attachment configuration. By changing the load operation to `eLoad`, we preserve the scene rendering results as the foundation for UI overlay rendering. This approach is more explicit and controllable than relying on automatic render pass dependencies. + +Removing the depth attachment for UI rendering eliminates unnecessary depth testing overhead, since UI elements typically render in screen space without complex occlusion relationships. This optimization can provide measurable performance improvements, especially on mobile GPUs where bandwidth is at a premium. + +=== Multi-Buffer: UI Rendering and Submission Coordination + +The final phase handles UI rendering execution and coordinates the submission of both command buffers in the correct order. + +[source,cpp] +---- + // Execute UI overlay rendering + // The custom renderer handles ImGui's dynamic rendering internally + renderer.render(ImGui::GetDrawData(), imguiCommandBuffer); + + // Finalize UI command buffer + imguiCommandBuffer.end(); + + // Coordinate submission of both command buffers in dependency order + // Scene must complete before UI to ensure proper overlay rendering + std::array submitCommandBuffers = { + *sceneCommandBuffer, // Execute scene rendering first + *imguiCommandBuffer // Then execute UI overlay + }; + + // Configure batch submission for optimal GPU utilization + vk::SubmitInfo submitInfo{}; + submitInfo.commandBufferCount = static_cast(submitCommandBuffers.size()); + submitInfo.pCommandBuffers = submitCommandBuffers.data(); + + // Submit both command buffers as a cohesive frame + // ... rest of your submission code ... +} +---- + +=== Handling Multiple Viewports + +ImGui supports multiple viewports, which allows UI windows to be detached from the main window. To support this feature, we need to handle additional steps: + +[source,cpp] +---- +// In your main loop, after rendering ImGui +if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + ImGui::UpdatePlatformWindows(); + ImGui::RenderPlatformWindowsDefault(); +} +---- + +This will render any detached ImGui windows. Note that this feature requires additional platform-specific code and may not be necessary for all applications. + +=== Handling Window Resize + +When the window is resized, you need to recreate the swap chain and update ImGui: + +[source,cpp] +---- +void recreateSwapChain() { + // ... existing swap chain recreation code ... + + // Update ImGui display size + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(swapChainExtent.width), + static_cast(swapChainExtent.height)); +} +---- + +=== Performance Considerations + +Here are some tips to optimize ImGui rendering performance in Vulkan: + +1. *Minimize State Changes*: Try to render all ImGui elements in a single pass to minimize state changes. + +2. *Use Appropriate Descriptor Pool Sizes*: Allocate enough descriptors for ImGui to avoid running out of descriptors. + +3. *Consider Secondary Command Buffers*: For complex UIs, consider using secondary command buffers to record ImGui commands in parallel. + +4. *Optimize UI Updates*: Only update UI elements that change, and consider using ImGui's `Begin()` function with the `ImGuiWindowFlags_NoDecoration` flag for static UI elements. + +5. *Use ImGui's Memory Allocators*: ImGui allows you to provide custom memory allocators, which can be useful for controlling memory usage. + +=== Complete Integration Example + +Let's put everything together in a complete example that integrates ImGui with a Vulkan application: + +[source,cpp] +---- +class VulkanApplication { +private: + // ... existing Vulkan members ... + + // ImGui-specific members + vk::raii::DescriptorPool imguiPool = nullptr; + bool showDemoWindow = true; + bool showMetricsWindow = false; + +public: + void initVulkan() { + // ... existing Vulkan initialization ... + + // Initialize ImGui + createImGuiDescriptorPool(); + initImGui(); + } + + void createImGuiDescriptorPool() { + // ImGui typically needs a handful of descriptors (font texture + user UI textures). + // Adjust these values to your app's needs (e.g., expected number of UI textures, buffers). + // As a starting point: + vk::DescriptorPoolSize poolSizes[] = + { + { vk::DescriptorType::eSampler, 8 }, + { vk::DescriptorType::eCombinedImageSampler, 128 }, // font + user-provided textures + { vk::DescriptorType::eSampledImage, 128 }, + { vk::DescriptorType::eStorageImage, 8 }, + { vk::DescriptorType::eUniformTexelBuffer, 8 }, + { vk::DescriptorType::eStorageTexelBuffer, 8 }, + { vk::DescriptorType::eUniformBuffer, 32 }, + { vk::DescriptorType::eStorageBuffer, 32 }, + { vk::DescriptorType::eUniformBufferDynamic, 16 }, + { vk::DescriptorType::eStorageBufferDynamic, 16 }, + { vk::DescriptorType::eInputAttachment, 8 } + }; + + // A conservative maxSets equals the sum of descriptor counts. + uint32_t maxSets = 0; + for (const auto& ps : poolSizes) maxSets += ps.descriptorCount; + + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = maxSets, + .poolSizeCount = static_cast(std::size(poolSizes)), + .pPoolSizes = poolSizes + }; + + imguiPool = vk::raii::DescriptorPool(device, poolInfo); + } + + void initImGui() { + // Initialize ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + + // Set up ImGui style + ImGui::StyleColorsDark(); + + // Initialize our custom backend + int width = static_cast(swapChainExtent.width); + int height = static_cast(swapChainExtent.height); + ImGuiPlatform::Init(width, height); + + // Initialize our custom ImGui Vulkan renderer with dynamic rendering + ImGuiVulkanRenderer renderer; + renderer.initialize( + *instance, + *physicalDevice, + *device, + graphicsFamily, + *graphicsQueue, + *imguiPool, + static_cast(swapChainImages.size()), + vk::SampleCountFlagBits::e1 + ); + + // Set up dynamic rendering info + vk::PipelineRenderingCreateInfo renderingInfo{}; + renderingInfo.colorAttachmentCount = 1; + vk::Format formats[] = { swapChainImageFormat }; + renderingInfo.pColorAttachmentFormats = formats; + renderer.setDynamicRenderingInfo(renderingInfo); + + // Upload ImGui fonts + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + renderer.uploadFonts(commandBuffer); + endSingleTimeCommands(commandBuffer); + } + + void drawFrame() { + // ... existing frame preparation code ... + + // Start the ImGui frame + ImGui::NewFrame(); + + // Create ImGui UI + createImGuiUI(); + + // Render ImGui + ImGui::Render(); + + // ... existing command buffer recording code ... + + // Begin dynamic rendering for scene + vk::RenderingAttachmentInfo colorAttachment{}; + colorAttachment.imageView = *swapChainImageViews[imageIndex]; + colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; + colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; + colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 1.0f}; + + vk::RenderingAttachmentInfo depthAttachment{}; + depthAttachment.imageView = *depthImageView; + depthAttachment.imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; + depthAttachment.loadOp = vk::AttachmentLoadOp::eClear; + depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare; + depthAttachment.clearValue.depthStencil = vk::ClearDepthStencilValue{1.0f, 0}; + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, swapChainExtent}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = 1; + renderingInfo.pColorAttachments = &colorAttachment; + renderingInfo.pDepthAttachment = &depthAttachment; + + commandBuffer.beginRendering(renderingInfo); + + // Render 3D scene + // ... your existing scene rendering code ... + + commandBuffer.endRendering(); + + // Render ImGui using our custom renderer + // ImGui will handle its own dynamic rendering internally + renderer.render(ImGui::GetDrawData(), commandBuffer); + + // ... existing command buffer submission code ... + } + + void createImGuiUI() { + // Menu bar + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Exit", "Alt+F4")) { + // Generic way to request application exit + requestApplicationExit(); + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("View")) { + ImGui::MenuItem("Demo Window", nullptr, &showDemoWindow); + ImGui::MenuItem("Metrics", nullptr, &showMetricsWindow); + ImGui::EndMenu(); + } + + ImGui::EndMainMenuBar(); + } + + // Demo window + if (showDemoWindow) { + ImGui::ShowDemoWindow(&showDemoWindow); + } + + // Metrics window + if (showMetricsWindow) { + ImGui::ShowMetricsWindow(&showMetricsWindow); + } + + // Custom windows + ImGui::Begin("Settings"); + + static float color[3] = { 0.5f, 0.5f, 0.5f }; + if (ImGui::ColorEdit3("Clear Color", color)) { + // Update clear color + clearColor = { color[0], color[1], color[2], 1.0f }; + } + + static int selectedModel = 0; + const char* models[] = { "Cube", "Sphere", "Teapot", "Custom Model" }; + if (ImGui::Combo("Model", &selectedModel, models, IM_ARRAYSIZE(models))) { + // Change model + loadModel(models[selectedModel]); + } + + ImGui::End(); + } + + void cleanup() { + // ... existing cleanup code ... + + // Cleanup ImGui + renderer.cleanup(); + ImGuiPlatform::Shutdown(); // Our custom platform backend + ImGui::DestroyContext(); + } +}; +---- + +=== Advanced Topics + +==== Custom Shaders for ImGui + +ImGui uses its own shaders for rendering, but you can customize them if needed: + +[source,cpp] +---- +// Create custom shader modules +vk::raii::ShaderModule customVertShaderModule = createShaderModule("custom_imgui_vert.spv"); +vk::raii::ShaderModule customFragShaderModule = createShaderModule("custom_imgui_frag.spv"); + +// Initialize our custom renderer with custom shaders and dynamic rendering +ImGuiVulkanRenderer renderer; +renderer.initialize( + *instance, + *physicalDevice, + *device, + queueFamily, + *queue, + *descriptorPool, + minImageCount, + imageCount, + vk::SampleCountFlagBits::e1 +); + +// Set up dynamic rendering info +vk::PipelineRenderingCreateInfo renderingInfo{}; +renderingInfo.colorAttachmentCount = 1; +vk::Format formats[] = { swapChainImageFormat }; +renderingInfo.pColorAttachmentFormats = formats; +renderer.setDynamicRenderingInfo(renderingInfo); + +// Set custom shaders +renderer.setCustomShaders( + customVertShaderModule, + customFragShaderModule +); +---- + +==== Rendering ImGui to a Texture + +You can render ImGui to a texture instead of directly to the screen, which can be useful for creating in-game UI elements: + +[source,cpp] +---- +// Create a texture to render ImGui to +vk::raii::Image imguiTargetImage = createImage( + width, height, + vk::Format::eR8G8B8A8Unorm, + vk::ImageTiling::eOptimal, + vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled +); + +// Create image view +vk::raii::ImageView imguiTargetImageView = createImageView( + imguiTargetImage, + vk::Format::eR8G8B8A8Unorm, + vk::ImageAspectFlagBits::eColor +); + +// Render ImGui to the texture using dynamic rendering +vk::RenderingAttachmentInfo colorAttachment{}; +colorAttachment.imageView = *imguiTargetImageView; +colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; +colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; +colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; +colorAttachment.clearValue.color = std::array{0.0f, 0.0f, 0.0f, 0.0f}; + +vk::RenderingInfo renderingInfo{}; +renderingInfo.renderArea = vk::Rect2D{{0, 0}, {width, height}}; +renderingInfo.layerCount = 1; +renderingInfo.colorAttachmentCount = 1; +renderingInfo.pColorAttachments = &colorAttachment; + +commandBuffer.beginRendering(renderingInfo); +renderer.render(ImGui::GetDrawData(), commandBuffer); +commandBuffer.endRendering(); + +// Later, use the texture in your 3D scene +// ... +---- + +==== Handling High DPI Displays + +For high DPI displays, you need to handle scaling correctly across different platforms: + +[source,cpp] +---- +// Cross-platform display scaling +void updateDisplayScale(int width, int height, float scaleX, float scaleY) { + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); + io.DisplayFramebufferScale = ImVec2(scaleX, scaleY); + + // Update our platform backend + ImGuiPlatform::SetDisplaySize(width, height); +} + +// Platform-specific implementations +// Here's an example using GLFW, but you can implement similar functions +// for any windowing library you choose to use + +void updateDisplayScaleWithGLFW(GLFWwindow* window) { + // Get the framebuffer size (which may differ from window size on high DPI displays) + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + // Get the content scale (DPI scaling factor) + float xscale, yscale; + glfwGetWindowContentScale(window, &xscale, &yscale); + + // Update ImGui with the correct display size and scale + updateDisplayScale(width, height, xscale, yscale); +} + +// With other windowing libraries, you would use their equivalent APIs +// to get the framebuffer size and DPI scaling factor + +---- + +=== ImGui Utility Class + +To encapsulate all the ImGui functionality in a way that works across different platforms, let's create a utility class similar to the one mentioned in the Vulkan-Samples repository: + +[source,cpp] +---- +// ImGuiUtil.h +#pragma once + +import vulkan_hpp; +#include +#include +#include + +class ImGuiUtil { +public: + // Initialize ImGui with Vulkan using dynamic rendering + static void Init( + vk::raii::Instance& instance, + vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Device& device, + uint32_t queueFamily, + vk::raii::Queue& queue, + uint32_t minImageCount, + uint32_t imageCount, + vk::Format swapChainImageFormat, + vk::SampleCountFlagBits msaaSamples = vk::SampleCountFlagBits::e1 + ); + + // Shutdown ImGui + static void Shutdown(); + + // Start a new frame + static void NewFrame(); + + // Render ImGui draw data to a command buffer + static void Render(vk::raii::CommandBuffer& commandBuffer); + + // Update display size + static void UpdateDisplaySize(int width, int height, float scaleX = 1.0f, float scaleY = 1.0f); + + // Process platform-specific input event + static bool ProcessInputEvent(void* event); + + // Set input callback + static void SetInputCallback(std::function callback); + +private: + // Create descriptor pool for ImGui + static void createDescriptorPool(); + + // Upload fonts + static void uploadFonts(); + + // Begin single-time commands + static vk::raii::CommandBuffer beginSingleTimeCommands(); + + // End single-time commands + static void endSingleTimeCommands(vk::raii::CommandBuffer& commandBuffer); + + // Vulkan objects + static vk::raii::Instance* instance; + static vk::raii::PhysicalDevice* physicalDevice; + static vk::raii::Device* device; + static uint32_t queueFamily; + static vk::raii::Queue* queue; + static vk::raii::DescriptorPool descriptorPool; + static vk::raii::CommandPool commandPool; + static vk::PipelineRenderingCreateInfo renderingInfo; + + // Input callback + static std::function inputCallback; + + // Initialization state + static bool initialized; +}; + +// ImGuiUtil.cpp +#include "ImGuiUtil.h" + +// Static member initialization +vk::raii::Instance* ImGuiUtil::instance = nullptr; +vk::raii::PhysicalDevice* ImGuiUtil::physicalDevice = nullptr; +vk::raii::Device* ImGuiUtil::device = nullptr; +uint32_t ImGuiUtil::queueFamily = 0; +vk::raii::Queue* ImGuiUtil::queue = nullptr; +vk::raii::DescriptorPool ImGuiUtil::descriptorPool = nullptr; +vk::raii::CommandPool ImGuiUtil::commandPool = nullptr; +vk::PipelineRenderingCreateInfo ImGuiUtil::renderingInfo{}; +std::function ImGuiUtil::inputCallback = nullptr; +bool ImGuiUtil::initialized = false; + +void ImGuiUtil::Init( + vk::raii::Instance& instance, + vk::raii::PhysicalDevice& physicalDevice, + vk::raii::Device& device, + uint32_t queueFamily, + vk::raii::Queue& queue, + uint32_t minImageCount, + uint32_t imageCount, + vk::Format swapChainImageFormat, + vk::SampleCountFlagBits msaaSamples +) { + ImGuiUtil::instance = &instance; + ImGuiUtil::physicalDevice = &physicalDevice; + ImGuiUtil::device = &device; + ImGuiUtil::queueFamily = queueFamily; + ImGuiUtil::queue = &queue; + + // Set up dynamic rendering info + renderingInfo.colorAttachmentCount = 1; + vk::Format formats[] = { swapChainImageFormat }; + renderingInfo.pColorAttachmentFormats = formats; + + // Create command pool for font upload + vk::CommandPoolCreateInfo poolInfo{ + .flags = vk::CommandPoolCreateFlagBits::eTransient, + .queueFamilyIndex = queueFamily + }; + commandPool = vk::raii::CommandPool(device, poolInfo); + + // Create descriptor pool + createDescriptorPool(); + + // Initialize ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + + // Set up ImGui style + ImGui::StyleColorsDark(); + + // Initialize our custom Vulkan renderer with dynamic rendering + renderer = ImGuiVulkanRenderer(); + renderer.initialize( + *instance, + *physicalDevice, + *device, + queueFamily, + *queue, + *descriptorPool, + minImageCount, + imageCount, + msaaSamples + ); + + // Set dynamic rendering info + renderer.setDynamicRenderingInfo(renderingInfo); + + // Upload fonts + uploadFonts(); + + initialized = true; +} + +void ImGuiUtil::Shutdown() { + if (!initialized) return; + + // Wait for device to finish operations + device->waitIdle(); + + // Cleanup ImGui + renderer.cleanup(); + ImGui::DestroyContext(); + + // Cleanup Vulkan resources + commandPool = nullptr; + descriptorPool = nullptr; + + // Reset pointers + instance = nullptr; + physicalDevice = nullptr; + device = nullptr; + queue = nullptr; + + initialized = false; +} + +void ImGuiUtil::NewFrame() { + if (!initialized) return; + + // Update ImGui IO with platform-specific input + ImGuiIO& io = ImGui::GetIO(); + + // Call input callback if registered + if (inputCallback) { + inputCallback(io); + } + + ImGui::NewFrame(); +} + +void ImGuiUtil::Render(vk::raii::CommandBuffer& commandBuffer) { + if (!initialized) return; + + ImGui::Render(); + renderer.render(ImGui::GetDrawData(), commandBuffer); +} + +void ImGuiUtil::UpdateDisplaySize(int width, int height, float scaleX, float scaleY) { + if (!initialized) return; + + ImGuiIO& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(static_cast(width), static_cast(height)); + io.DisplayFramebufferScale = ImVec2(scaleX, scaleY); +} + +bool ImGuiUtil::ProcessInputEvent(void* event) { + // Platform-specific event processing would go here + // This is a placeholder for the actual implementation + return false; +} + +void ImGuiUtil::SetInputCallback(std::function callback) { + inputCallback = callback; +} + +void ImGuiUtil::createDescriptorPool() { + // Tune these to match your expected number of UI textures and buffers. + vk::DescriptorPoolSize poolSizes[] = + { + { vk::DescriptorType::eSampler, 8 }, + { vk::DescriptorType::eCombinedImageSampler, 128 }, + { vk::DescriptorType::eSampledImage, 128 }, + { vk::DescriptorType::eStorageImage, 8 }, + { vk::DescriptorType::eUniformTexelBuffer, 8 }, + { vk::DescriptorType::eStorageTexelBuffer, 8 }, + { vk::DescriptorType::eUniformBuffer, 32 }, + { vk::DescriptorType::eStorageBuffer, 32 }, + { vk::DescriptorType::eUniformBufferDynamic, 16 }, + { vk::DescriptorType::eStorageBufferDynamic, 16 }, + { vk::DescriptorType::eInputAttachment, 8 } + }; + + uint32_t maxSets = 0; + for (const auto& ps : poolSizes) maxSets += ps.descriptorCount; + + vk::DescriptorPoolCreateInfo poolInfo{ + .flags = vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet, + .maxSets = maxSets, + .poolSizeCount = static_cast(std::size(poolSizes)), + .pPoolSizes = poolSizes + }; + + descriptorPool = vk::raii::DescriptorPool(*device, poolInfo); +} + +void ImGuiUtil::uploadFonts() { + vk::raii::CommandBuffer commandBuffer = beginSingleTimeCommands(); + renderer.uploadFonts(commandBuffer); + endSingleTimeCommands(commandBuffer); +} + +vk::raii::CommandBuffer ImGuiUtil::beginSingleTimeCommands() { + vk::CommandBufferAllocateInfo allocInfo{ + .commandPool = *commandPool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = 1 + }; + + vk::raii::CommandBuffer commandBuffer = vk::raii::CommandBuffers(*device, allocInfo).front(); + + vk::CommandBufferBeginInfo beginInfo{ + .flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit + }; + + commandBuffer.begin(beginInfo); + + return commandBuffer; +} + +void ImGuiUtil::endSingleTimeCommands(vk::raii::CommandBuffer& commandBuffer) { + commandBuffer.end(); + + vk::SubmitInfo submitInfo{ + .commandBufferCount = 1, + .pCommandBuffers = &*commandBuffer + }; + + queue->submit(submitInfo); + queue->waitIdle(); +} +---- + +=== Conclusion + +In this section, we've explored how to integrate ImGui with Vulkan, including command buffer integration, render pass configuration, and performance considerations. By creating a flexible implementation, we've ensured that our GUI system works well with any windowing system you choose. + +The key improvements we've made include: + +1. Creating a platform-agnostic integration approach +2. Implementing a flexible input system that works with various windowing libraries +3. Developing a versatile ImGui utility class +4. Designing a window-system-independent integration + +With this knowledge, you can create a robust GUI system for your Vulkan application that provides a smooth user experience regardless of which windowing system you use. + +In the next section, we'll wrap up with a conclusion and discuss potential improvements to our GUI system. + +link:04_ui_elements.adoc[Previous: UI Elements] | link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc b/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc new file mode 100644 index 00000000..b1929d37 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/06_conclusion.adoc @@ -0,0 +1,113 @@ +:pp: {plus}{plus} + += GUI: Conclusion + +== Conclusion + +In this chapter, we've built a comprehensive GUI system for our Vulkan application using Dear ImGui. Let's summarize what we've learned and discuss potential improvements. + +=== What We've Learned + +* *Flexible ImGui Setup*: We explored how to integrate Dear ImGui with Vulkan in a way that works across different platforms, including desktop and mobile. We created an implementation that doesn't rely on specific windowing systems like GLFW. + +* *Versatile Input Handling*: We implemented a robust input handling system that correctly routes input events to either the GUI or the 3D scene, ensuring a smooth user experience on any device. + +* *UI Elements*: We learned how to create various UI elements, from basic components like buttons and sliders to more complex elements like tables and plots, and how to organize them into a cohesive interface that works well on both desktop and mobile platforms. + +* *Vulkan Integration*: We dove deep into the technical details of integrating ImGui with the Vulkan rendering pipeline, including command buffer integration, render pass configuration, and performance considerations. + +With these components in place, we now have a solid foundation for creating interactive applications with Vulkan that can run on multiple platforms. Our GUI system allows users to control settings, display information, and interact with the 3D scene through an intuitive interface, whether they're using a desktop computer, a mobile phone, or a tablet. + +=== Potential Improvements + +While our GUI system is functional, there are several ways it could be enhanced: + +* *Targeted Optimizations*: Implement specific optimizations for better performance on each target platform. + +* *Touch-Friendly UI*: Enhance the UI elements to be more touch-friendly for mobile platforms, with larger hit areas and gesture support. + +* *Adaptive Layouts*: Create layouts that automatically adapt to different screen sizes and orientations, from desktop monitors to mobile phones. + +* *Custom Styling*: Create a custom theme that matches your application's visual style, rather than using the default ImGui style. + +* *Localization*: Add support for multiple languages by implementing a localization system for UI text. + +* *Accessibility*: Improve accessibility by adding features like keyboard navigation, screen reader support, and high-contrast modes. + +* *Persistent Settings*: Implement a system to save and load UI settings between application sessions. + +* *Advanced Layout*: Use ImGui's docking features to create more complex UI layouts, such as dockable panels. + +* *Custom Widgets*: Develop custom widgets for specific needs in your application, such as a color wheel, a curve editor, or a node graph editor. + +* *Performance Optimization*: Profile and optimize the GUI rendering to minimize its impact on overall application performance, especially on mobile devices with limited resources. + +* *Battery Efficiency*: For mobile platforms, optimize the GUI rendering to minimize battery usage. + +=== Integration with Other Systems + +As you continue building your Vulkan engine, consider how the GUI system integrates with other components: + +* *Scene Graph*: How can the GUI be used to visualize and edit the scene graph hierarchy across different platforms? + +* *Material System*: Can you create a material editor using the GUI to adjust material properties in real-time, with interfaces that work well on both desktop and mobile? + +* *Animation System*: How might the GUI be used to control and visualize animations, with controls that are appropriate for each platform? + +* *Physics System*: Could the GUI provide tools for setting up and debugging physics simulations, with different interaction models for desktop and mobile? + +* *Device-Specific Features*: How can you leverage specific features (like haptic feedback on mobile) while maintaining a consistent core experience? + +By addressing these questions, you can create a more cohesive and powerful engine that leverages the GUI for both development and runtime functionality across multiple platforms. + +=== Cross-Platform Considerations + +When developing a GUI system that works across platforms, keep these considerations in mind: + +* *Input Methods*: Different platforms have different primary input methods (mouse/keyboard vs. touch). + +* *Screen Sizes*: Interfaces need to work on screens ranging from small phones to large monitors. + +* *Performance Constraints*: Mobile devices typically have less processing power and memory than desktops. + +* *Battery Life*: On mobile devices, efficient rendering is crucial for battery life. + +* *Platform Conventions*: Users expect applications to follow platform-specific UI conventions. + +* *Testing*: Cross-platform applications require testing on all target platforms. + +=== Alternative GUI Libraries for Vulkan + +While we've focused on https://github.com/ocornut/imgui[Dear ImGui] in this chapter, there are several other GUI libraries that work well with Vulkan. Understanding the options can help you choose the right tool for your specific needs: + +* https://github.com/Immediate-Mode-UI/Nuklear[*Nuklear*]: A minimalist immediate-mode GUI library with a small footprint. It's designed to be embedded directly into applications and supports Vulkan among other rendering backends. Nuklear is used in smaller indie games and tools due to its simplicity and low overhead. + +* https://www.qt.io/[*Qt*]: A comprehensive UI framework that added Vulkan support in Qt 5.10. Qt provides a more traditional retained-mode GUI approach with a rich set of widgets and tools. It's used in applications like the Autodesk Maya viewport and various CAD software. + +* http://cegui.org.uk/[*CEGUI*]: The Crazy Eddie's GUI system is a free library providing windowing and widgets for games and simulation applications. It has Vulkan renderer support and is used in some indie game engines. + +* https://ultralig.ht/[*Ultralight*]: A lightweight, high-performance HTML renderer designed for game and application UIs. It can be integrated with Vulkan and is used by developers who want to leverage web technologies for their interfaces. + +* https://www.noesisengine.com/[*Noesis GUI*]: A commercial UI middleware that supports XAML and can render through Vulkan. It's used in games like Dauntless and provides a designer-friendly workflow. + +When choosing a GUI library for your Vulkan application, consider factors like: + +* Development paradigm (immediate-mode vs. retained-mode) +* Performance requirements +* Designer-friendliness +* Learning curve +* Licensing and cost +* Platform support +* Integration complexity + +Dear ImGui, which we've used in this chapter, strikes a good balance for many developers due to its simplicity, performance, and ease of integration with Vulkan. + +=== Final Thoughts + +A well-designed GUI is essential for creating user-friendly applications that can reach a wide audience. It serves as the primary way users interact with your application and can significantly impact the user experience. By understanding how to integrate Dear ImGui with Vulkan and implementing a robust input handling system that works with basic inputs for mouse and keyboard, you've taken a major step toward creating professional-quality applications. + +Remember that the code provided in this chapter is a starting point. Feel free to modify and extend it to suit your specific needs and application requirements. The flexibility of our approach allows for a wide range of customization and extension while maintaining compatibility with multiple platforms. + +In the next chapter, we'll explore how to load and render 3D models, which will allow us to create more complex and visually interesting scenes. + +link:05_vulkan_integration.adoc[Previous: Vulkan Integration] | link:../Loading_Models/01_introduction.adoc[Next: Loading Models] diff --git a/en/Building_a_Simple_Engine/GUI/index.adoc b/en/Building_a_Simple_Engine/GUI/index.adoc new file mode 100644 index 00000000..1578b7f9 --- /dev/null +++ b/en/Building_a_Simple_Engine/GUI/index.adoc @@ -0,0 +1,15 @@ +:pp: {plus}{plus} + += GUI: Introduce working with a GUI and handling input + +include::01_introduction.adoc[] + +include::02_imgui_setup.adoc[] + +include::03_input_handling.adoc[] + +include::04_ui_elements.adoc[] + +include::05_vulkan_integration.adoc[] + +include::06_conclusion.adoc[] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc new file mode 100644 index 00000000..4b316387 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/01_introduction.adoc @@ -0,0 +1,184 @@ += Introduction to Lighting & Materials + +In this chapter, we'll explore the fundamentals of lighting and materials in 3D rendering, with a focus on Physically Based Rendering (PBR). Lighting is a crucial aspect of creating realistic and visually appealing 3D scenes. Without proper lighting, even the most detailed models can appear flat and lifeless. + +image:../../../images/bistro.png[Bistro scene with PBR, width=600, alt=Rendering the Bistro scene at night with PBR pass] + + +[NOTE] +==== +*About PBR References*: Throughout this tutorial, you may encounter references to PBR (Physically Based Rendering) before reaching this chapter. PBR is a modern rendering approach that simulates how light interacts with surfaces based on physical principles. We'll cover PBR in detail in the sections that follow, so don't worry if you're not familiar with these concepts yet. +==== + +This chapter serves as the foundation for understanding how light interacts with different materials in a physically accurate way. The concepts you'll learn here will be applied in later chapters, including the Loading_Models chapter where we'll use this knowledge to render glTF models with PBR materials. + +Throughout our engine implementation, we'll be using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +== Why Lighting Matters + +Lighting in computer graphics serves several important purposes: + +1. *Visual Realism*: Proper lighting creates shadows, highlights, and gradients that make 3D objects appear more realistic. +2. *Spatial Understanding*: Lighting helps viewers understand the spatial relationships between objects in a scene. +3. *Mood and Atmosphere*: Different lighting setups can dramatically change the mood and atmosphere of a scene. +4. *Focus and Attention*: Lighting can be used to draw attention to important elements in a scene. + +== Physically Based Rendering (PBR) + +=== Introduction to PBR + +Physically Based Rendering (PBR) represents one of the most significant advancements in real-time graphics over the past decade. Unlike traditional rendering approaches that used ad-hoc shading models, PBR aims to simulate how light interacts with surfaces in the real world based on the principles of physics. + +=== The Evolution of Real-Time Rendering + +To appreciate PBR, it helps to understand how real-time rendering has evolved: + +1. *Fixed-Function Pipeline (1990s)*: Early 3D hardware used fixed lighting models like Gouraud or Phong shading with limited material properties. + +2. *Programmable Shaders (2000s)*: With the introduction of shader programming, developers could implement custom lighting models, but these were often inconsistent across different lighting conditions. + +3. *Physically Based Rendering (2010s)*: By basing rendering on physical principles, PBR provides more realistic results that remain consistent across different environments. + +The key advantages of PBR include: + +* *Realism*: Materials look correct under any lighting condition +* *Consistency*: Artists can create materials that work in all environments +* *Intuitiveness*: Material parameters have physical meaning, making them easier to understand +* *Efficiency*: Modern PBR implementations are optimized for real-time performance + +=== Core Principles of PBR + +PBR is built on several key principles that distinguish it from earlier rendering approaches: + +==== Energy Conservation + +In the real world, a surface cannot reflect more light than it receives. This principle of energy conservation is fundamental to PBR: + +* The sum of diffuse and specular reflection must not exceed 1.0 +* As surfaces become more metallic, they have less diffuse reflection +* As surfaces become rougher, specular highlights become larger but less intense + +==== Microfacet Theory + +PBR uses microfacet theory to model surface roughness. This theory assumes that surfaces are composed of tiny, perfectly reflective microfacets with varying orientations: + +* Smooth surfaces have microfacets that are mostly aligned, creating sharp reflections +* Rough surfaces have randomly oriented microfacets, scattering light and creating blurry reflections +* The distribution of these microfacets is controlled by the roughness parameter + +==== Fresnel Effect + +The Fresnel effect describes how reflectivity changes with viewing angle: + +* All surfaces become more reflective at grazing angles (angles where the viewing direction is nearly parallel to the surface) +* This effect is more noticeable on smooth surfaces +* The base reflectivity at normal incidence (F0, when light hits the surface perpendicularly), is determined by the material's index of refraction +* For metals, F0 is colored (based on the metal's properties) +* For non-metals (dielectrics), F0 is typically around 0.04 (4%) + +==== Metallic-Roughness Workflow + +The PBR implementation in glTF and many modern engines uses the metallic-roughness workflow, which defines materials using these primary parameters: + +* *Base Color*: The albedo or diffuse color of the surface +* *Metallic*: How "metal-like" the surface is (0.0 = non-metal, 1.0 = metal) +* *Roughness*: How smooth or rough the surface is (0.0 = mirror-like, 1.0 = rough) + +This workflow is intuitive for artists and efficient for real-time rendering. + +=== The BRDF in PBR + +The Bidirectional Reflectance Distribution Function (BRDF) is at the heart of PBR. It describes how light is reflected from a surface, taking into account: + +* The incoming light direction +* The outgoing view direction +* The surface normal +* The material properties + +In PBR, the BRDF is typically split into two components: + +* *Diffuse BRDF*: Handles light that penetrates the surface, scatters, and exits +* *Specular BRDF*: Handles light that reflects directly from the surface + +==== Diffuse BRDF + +The simplest diffuse BRDF is the Lambertian model: + +[source] +---- +f_diffuse = albedo / π +---- + +Where: +* albedo is the base color of the surface +* π is a normalization factor + +More advanced models like Disney's diffuse or Oren-Nayar can be used for increased realism, especially for rough surfaces. + +==== Specular BRDF + +For the specular component, PBR typically uses a microfacet BRDF: + +[source] +---- +f_specular = D * F * G / (4 * (n·ωo) * (n·ωi)) +---- + +Where: +* D is the Normal Distribution Function (NDF) +* F is the Fresnel term +* G is the Geometry term +* n is the surface normal +* ωo is the outgoing (view) direction +* ωi is the incoming (light) direction + +Popular implementations include: +* *D*: GGX (Trowbridge-Reitz) distribution +* *F*: Schlick's approximation +* *G*: Smith shadowing-masking function + +== Materials in Computer Graphics + +Materials define how surfaces interact with light. Different materials reflect, absorb, and transmit light in different ways. Understanding materials is crucial for creating realistic renderings. + +=== Material Properties + +In computer graphics, materials are defined by various properties: + +* *Base Color/Albedo*: The color of the surface under diffuse lighting +* *Metalness*: How metallic the surface is (affects specular reflection and diffuse absorption) +* *Roughness/Smoothness*: How rough or smooth the surface is (affects specular highlight size and sharpness) +* *Normal Map*: Adds surface detail without increasing geometric complexity +* *Ambient Occlusion*: Approximates how much ambient light a surface point receives +* *Emissive*: Makes parts of the surface emit light +* *Opacity/Transparency*: Controls how transparent the material is +* *Refraction*: Controls how light bends when passing through the material + +=== Common Material Types + +Different types of materials have different characteristics: + +* *Metals*: High specular reflection, colored specular, no diffuse reflection +* *Dielectrics (Non-metals)*: Lower specular reflection, white specular, strong diffuse reflection +* *Translucent Materials*: Allow light to pass through and scatter within (e.g., skin, wax, marble) +* *Transparent Materials*: Allow light to pass through with minimal scattering (e.g., glass, water) +* *Anisotropic Materials*: Reflect light differently based on direction (e.g., brushed metal, hair) + +=== Push Constants for Material Properties + +In our implementation, we'll use push constants to efficiently pass material properties to our shaders. + +Push constants are a way to send a small amount of data to shaders without having to create and manage descriptor sets. They're perfect for frequently changing data like material properties. + +== What You'll Learn + +By the end of this chapter, you'll understand: + +1. How Physically Based Rendering works +2. How to implement PBR in Slang shaders +3. How to use push constants for material properties +4. How to integrate PBR lighting with Vulkan + +Let's get started by exploring the principles of Physically Based Rendering in more detail. + +link:../Camera_Transformations/06_conclusion.adoc[Previous: Camera Transformations - Conclusion] | link:02_lighting_models.adoc[Next: Lighting Models] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc new file mode 100644 index 00000000..c726feb0 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/02_lighting_models.adoc @@ -0,0 +1,189 @@ += Lighting Models + +In this section, we'll explore various lighting models used in computer graphics, with a focus on understanding the concepts rather than implementation details. We'll discuss how different lighting models simulate the interaction of light with surfaces, their advantages and limitations, and when to use each approach. + +In this chapter, we'll introduce Physically Based Rendering (PBR) and other lighting models. The concepts we cover here will be applied in later chapters, such as the Loading_Models chapter where we'll use glTF, which uses PBR with the metallic-roughness workflow for its material system. By understanding the theory behind different lighting models, including PBR, we can better leverage the material properties provided by glTF models and extend our rendering capabilities. + +Throughout our engine implementation, we'll be using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +== Understanding Light-Surface Interaction + +Before diving into specific lighting models, it's important to understand how light interacts with surfaces in the real world: + +* *Reflection*: Light bounces off the surface +* *Absorption*: Light is absorbed by the surface and converted to heat +* *Transmission*: Light passes through the surface (for transparent materials) +* *Scattering*: Light is scattered in various directions within the material + +The way these interactions occur depends on the material properties and the characteristics of the light. + +=== Types of Reflection + +There are two main types of reflection: + +* *Diffuse Reflection*: Light is scattered in many directions, creating a matte appearance +* *Specular Reflection*: Light is reflected in a specific direction, creating highlights + +Most real-world materials exhibit a combination of diffuse and specular reflection. + +== The Evolution of Lighting Models + +Lighting models in computer graphics have evolved significantly over time, each with their own approach to simulating light-surface interactions: + +=== Early Lighting Models + +==== Flat Shading + +The simplest lighting model, where each polygon is assigned a single color based on its normal and the light direction. This creates a faceted appearance with visible polygon edges. + +* *Advantages*: Very fast to compute +* *Disadvantages*: Unrealistic appearance, visible polygon edges +* *When to use*: For very low-power devices or stylized rendering + +==== Gouraud Shading + +An improvement over flat shading, where lighting is calculated at the vertices and then interpolated across the polygon. + +* *Advantages*: Smoother appearance than flat shading, still relatively fast +* *Disadvantages*: Cannot accurately represent specular highlights +* *When to use*: For low-power devices where Phong shading is too expensive + +==== Phong Lighting Model + +One of the most widely used traditional lighting models, developed by Bui Tuong Phong in 1975. It calculates lighting using three components: + +* *Ambient*: A constant light level to simulate indirect lighting +* *Diffuse*: Light scattered in all directions (using Lambert's cosine law) +* *Specular*: Shiny highlights (using a power function of the reflection vector and view vector) + +* *Advantages*: Reasonably realistic for many materials, intuitive parameters +* *Disadvantages*: Not physically accurate, can look artificial under certain lighting conditions +* *When to use*: For simple real-time applications where PBR is too expensive + +For more information on the Phong lighting model, see the link:https://en.wikipedia.org/wiki/Phong_reflection_model[Wikipedia article]. + +==== Blinn-Phong Model + +A modification of the Phong model by Jim Blinn that uses the halfway vector between the light and view directions instead of the reflection vector, making it more efficient to compute. + +* *Advantages*: Faster than Phong, similar visual results +* *Disadvantages*: Still not physically accurate +* *When to use*: As a more efficient alternative to Phong + +Learn more about Blinn-Phong in this link:https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model[Wikipedia article] or this link:https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-5-implementing-improved-perlin-noise[GPU Gems chapter]. + +=== Advanced Lighting Models + +==== Cook-Torrance Model + +A more physically-based model developed by Robert Cook and Kenneth Torrance in 1982. It uses microfacet theory to model surface roughness and includes a more accurate specular term. + +* *Advantages*: More physically accurate than Phong or Blinn-Phong +* *Disadvantages*: More complex to implement and compute +* *When to use*: When you need more realistic materials but full PBR is too expensive + +For more details, see the original link:https://graphics.pixar.com/library/ReflectanceModel/paper.pdf[Cook-Torrance paper]. + +==== Oren-Nayar Model + +An extension of the Lambertian diffuse model that accounts for microfacet roughness in diffuse reflection, making it more suitable for rough surfaces like cloth, concrete, or sand. + +* *Advantages*: More realistic diffuse reflection for rough surfaces +* *Disadvantages*: More expensive than Lambertian diffuse +* *When to use*: For materials where diffuse roughness is important + +Learn more in the original link:https://www1.cs.columbia.edu/CAVE/publications/pdfs/Oren_SIGGRAPH94.pdf[Oren-Nayar paper]. + +==== Physically Based Rendering (PBR) + +PBR represents one of the most significant advancements in real-time graphics over the past decade. Unlike earlier ad-hoc shading models, PBR aims to simulate how light interacts with surfaces based on the principles of physics. + +The key principles of PBR include: + +* *Energy Conservation*: A surface cannot reflect more light than it receives +* *Microfacet Theory*: Surfaces are modeled as collections of tiny mirrors with varying orientations +* *Fresnel Effect*: Reflectivity changes with viewing angle +* *Metallic-Roughness Workflow*: Materials are defined by their base color, metalness, and roughness + +Considerations for using PBR: + +* *Advantages*: Realistic results that remain consistent across different lighting conditions, intuitive parameters for artists +* *Disadvantages*: More complex and computationally expensive +* *When to use*: For modern games and applications where realism is important + +For comprehensive information on PBR, see the link:https://www.pbr-book.org/[Physically Based Rendering book]. + +== Lighting Models in glTF + +The glTF format uses PBR with the metallic-roughness workflow, which defines materials using these primary parameters: + +* *Base Color*: The albedo or diffuse color of the surface +* *Metallic*: How "metal-like" the surface is (0.0 = non-metal, 1.0 = metal) +* *Roughness*: How smooth or rough the surface is (0.0 = mirror-like, 1.0 = rough) + +This workflow is intuitive for artists and efficient for real-time rendering. The glTF specification provides a standardized way to define PBR materials that can be used across different rendering engines. + +For more information on the glTF PBR implementation, see the link:https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials[glTF 2.0 specification]. + +== Light Types + +Different lighting models can work with various types of light sources: + +1. *Point Lights*: Light emanates in all directions from a single point. +2. *Directional Lights*: Light rays are parallel, as if coming from a very distant source (like the sun). +3. *Spot Lights*: Light is emitted in a cone shape from a point. +4. *Area Lights*: Light is emitted from a surface area. +5. *Image-Based Lighting (IBL)*: Light is derived from an environment map, simulating global illumination. + +Each type of light requires specific calculations for the light direction, attenuation, and other properties. + +== Advanced Lighting Techniques + +Beyond basic lighting models, there are several advanced techniques that can enhance the realism of your rendering: + +=== Global Illumination + +Global Illumination (GI) simulates how light bounces between surfaces, creating indirect lighting effects. Techniques include: + +* *Radiosity*: Calculates diffuse light transfer between surfaces +* *Path Tracing*: Traces light paths through the scene +* *Photon Mapping*: Stores light information in a spatial data structure + +For more information, see this link:https://developer.download.nvidia.com/books/HTML/gpugems2/chapters/gpugems2_chapter12.html[GPU Gems chapter on radiosity]. + +=== Subsurface Scattering + +Subsurface Scattering (SSS) simulates how light penetrates and scatters within translucent materials like skin, wax, or marble. + +For more information, see this link:https://developer.nvidia.com/gpugems/gpugems/part-iii-materials/chapter-16-real-time-approximations-subsurface-scattering[GPU Gems chapter on subsurface scattering]. + +=== Ambient Occlusion + +Ambient Occlusion (AO) approximates how much ambient light a surface point would receive, darkening corners and crevices. + +For more information, see this link:https://developer.download.nvidia.com/books/HTML/gpugems/gpugems_ch17.html[GPU Gems chapter on ambient occlusion]. + +== Choosing the Right Lighting Model + +When deciding which lighting model to use for your application, consider: + +1. *Hardware Constraints*: More complex models require more processing power +2. *Visual Requirements*: How realistic do your materials need to look? +3. *Artist Workflow*: Some models are more intuitive for artists to work with +4. *Consistency*: PBR provides more consistent results across different lighting conditions + +For our engine, we'll leverage the PBR implementation from the glTF format, as it provides a good balance of realism, performance, and artist-friendly parameters. + +== Further Reading + +To deepen your understanding of lighting models, here are some valuable resources: + +* link:https://www.pbr-book.org/[Physically Based Rendering: From Theory to Implementation] - The definitive book on PBR +* link:https://learnopengl.com/PBR/Theory[LearnOpenGL PBR Tutorial] - An accessible introduction to PBR concepts +* link:https://google.github.io/filament/Filament.html[Filament Material System] - Google's real-time PBR rendering engine documentation +* link:https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials[glTF 2.0 Material Specification] - Details on how PBR is implemented in glTF +* link:https://developer.nvidia.com/gpugems/gpugems/part-iii-materials[GPU Gems: Materials] - Collection of articles on advanced material rendering + +In the next section, we'll explore how to use push constants to efficiently pass material properties to our shaders. + +link:01_introduction.adoc[Previous: Introduction] | link:03_push_constants.adoc[Next: Push Constants] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc new file mode 100644 index 00000000..46957165 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/03_push_constants.adoc @@ -0,0 +1,130 @@ += Push Constants + +In this section, we'll explore push constants, a powerful feature in Vulkan that allows us to efficiently pass small amounts of data to shaders without the overhead of descriptor sets. + +== What Are Push Constants? + +Push constants are a way to send a small amount of data directly to shaders. Unlike uniform buffers, which require descriptor sets and memory allocation, push constants are part of the command buffer itself. This makes them ideal for small, frequently changing data. + +Some key characteristics of push constants: they are tiny (typically up to 128 bytes, device dependent), fast to update per draw, and require no descriptor sets or allocations because they live on the command buffer. They can be read by any shader stage you enable in the pipeline. + +== When to Use Push Constants + +Use push constants for tiny, per‑draw parameters that change frequently—exactly the kind of material knobs (base color, metallic, roughness) we tweak per object. If the data is larger than the device’s push‑constant limit or doesn’t change often, prefer a uniform buffer instead. + +== Defining Push Constants in Shaders + +In GLSL (or SPIR-V), push constants are defined using a uniform block with the `push_constant` layout qualifier: + +[source,glsl] +---- +layout(push_constant) uniform PushConstants { + vec4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; +} material; +---- + +In Slang, which we're using for our engine, the syntax is slightly different: + +[source,slang] +---- +struct PushConstants { + float4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; +}; + +[[vk::push_constant]] PushConstants material; +---- + +== Setting Up Push Constants in Vulkan + +To use push constants in Vulkan with vk::raii, we need to: + +1. Define a push constant range when creating the pipeline layout. +2. Use `commandBuffer.pushConstants` to send data to the shader. + +Here's how we define a push constant range: + +[source,cpp] +---- +// Set up push constant range for material properties +vk::PushConstantRange pushConstantRange; +pushConstantRange.setStageFlags(vk::ShaderStageFlagBits::eFragment) // Which shader stages can access the push constants + .setOffset(0) + .setSize(sizeof(PushConstantBlock)); // Size of our push constant data + +// Create pipeline layout with push constants +vk::PipelineLayoutCreateInfo pipelineLayoutInfo; +pipelineLayoutInfo.setSetLayoutCount(1) + .setPSetLayouts(&*descriptorSetLayout) + .setPushConstantRangeCount(1) + .setPPushConstantRanges(&pushConstantRange); + +// Create pipeline layout with vk::raii +vk::raii::PipelineLayout pipelineLayout = device.createPipelineLayout(pipelineLayoutInfo); +---- + +And here's how we send data to the shader: + +[source,cpp] +---- +// Define material properties +PushConstantBlock pushConstants{}; +pushConstants.baseColorFactor = {1.0f, 1.0f, 1.0f, 1.0f}; +pushConstants.metallicFactor = 1.0f; +pushConstants.roughnessFactor = 0.5f; +pushConstants.baseColorTextureSet = 0; +pushConstants.physicalDescriptorTextureSet = 1; +pushConstants.normalTextureSet = 2; +pushConstants.occlusionTextureSet = 3; +pushConstants.emissiveTextureSet = 4; +pushConstants.alphaMask = 0.0f; +pushConstants.alphaMaskCutoff = 0.5f; + +// Push constants to shader using vk::raii +commandBuffer.pushConstants( + *pipelineLayout, + vk::ShaderStageFlagBits::eFragment, // Which shader stages will receive the data + 0, // Offset + sizeof(PushConstantBlock), // Size + &pushConstants // Data +); +---- + +== Push Constants vs. Uniform Buffers + +While push constants are efficient for small, frequently changing data, they have limitations. For larger data sets or data that doesn't change frequently, uniform buffers are often a better choice. + +Here's a comparison: + +|=== +| Feature | Push Constants | Uniform Buffers +| Size | Limited (typically 128 bytes) | Much larger +| Update Mechanism | Direct command in command buffer | Memory mapping or staging buffer +| Descriptor Sets | Not required | Required +| Memory Allocation | Not required | Required +| Update Frequency | Ideal for frequent updates | Better for infrequent updates +| Access Speed | Fast | Slightly slower +|=== + +For our PBR implementation, we'll use push constants for material properties and uniform buffers for light information and transformation matrices. + +In the next section, we'll implement a basic lighting shader that uses push constants for material properties. + +link:02_lighting_models.adoc[Previous: Lighting Models] | link:04_lighting_implementation.adoc[Next: Lighting Implementation] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc new file mode 100644 index 00000000..1149e64c --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/04_lighting_implementation.adoc @@ -0,0 +1,786 @@ += PBR Lighting Implementation + +In this section, we'll implement a Physically Based Rendering (PBR) shader based on the concepts we've explored in the previous sections. This shader will use the metallic-roughness workflow that's compatible with glTF models and push constants for material properties. We'll examine the shader implementation and then discuss how to integrate it with our engine. + +== Implementing the PBR Shader + +Let's create a PBR shader, which we'll name `pbr.slang`. This shader implements the metallic-roughness workflow that we've discussed, making it compatible with glTF models. It uses push constants for material properties and uniform buffers for transformation matrices and light information. + +We'll break this shader into three distinct sections to better understand its architecture: + +=== Section 1: Shader Setup Code - CPU-GPU Communication + +This section establishes the communication interface between the CPU application and GPU shader. It defines the data structures and bindings that allow the CPU to pass information to the GPU efficiently. + +[source,slang] +---- +// Combined vertex and fragment shader for PBR rendering + +// Input from vertex buffer - Data sent per vertex from CPU +struct VSInput { + float3 Position : POSITION; // 3D position in model space + float3 Normal : NORMAL; // Surface normal for lighting calculations + float2 UV : TEXCOORD0; // Texture coordinates for material sampling + float4 Tangent : TANGENT; // Tangent vector for normal mapping (w component = handedness) +}; + +// Output from vertex shader / Input to fragment shader - Interpolated data +struct VSOutput { + float4 Position : SV_POSITION; // Required clip space position for rasterization + float3 WorldPos : POSITION; // World space position for lighting calculations + float3 Normal : NORMAL; // World space normal (interpolated) + float2 UV : TEXCOORD0; // Texture coordinates (interpolated) + float4 Tangent : TANGENT; // World space tangent (interpolated) +}; + +// Uniform buffer - Global data shared across all vertices/fragments +struct UniformBufferObject { + float4x4 model; // Model-to-world transformation matrix + float4x4 view; // World-to-camera transformation matrix + float4x4 proj; // Camera-to-clip space projection matrix + float4 lightPositions[4]; // Light positions in world space + float4 lightColors[4]; // Light intensities and colors + float4 camPos; // Camera position for view-dependent effects + float exposure; // HDR exposure control + float gamma; // Gamma correction value (typically 2.2) + float prefilteredCubeMipLevels; // IBL prefiltered environment map mip levels + float scaleIBLAmbient; // IBL ambient contribution scale +}; + +// Push constants - Fast, small data updated frequently per material/object +struct PushConstants { + float4 baseColorFactor; // Base color tint/multiplier + float metallicFactor; // Metallic property multiplier + float roughnessFactor; // Surface roughness multiplier + int baseColorTextureSet; // Texture binding index for base color (-1 = none) + int physicalDescriptorTextureSet; // Texture binding for metallic/roughness + int normalTextureSet; // Texture binding for normal maps + int occlusionTextureSet; // Texture binding for ambient occlusion + int emissiveTextureSet; // Texture binding for emissive maps + float alphaMask; // Alpha masking enable flag + float alphaMaskCutoff; // Alpha cutoff threshold +}; + +// Mathematical constants +static const float PI = 3.14159265359; + +// Resource bindings - Connect CPU resources to GPU shader registers +[[vk::binding(0, 0)]] ConstantBuffer ubo; +[[vk::binding(1, 0)]] Texture2D baseColorMap; +[[vk::binding(1, 0)]] SamplerState baseColorSampler; +[[vk::binding(2, 0)]] Texture2D metallicRoughnessMap; +[[vk::binding(2, 0)]] SamplerState metallicRoughnessSampler; +[[vk::binding(3, 0)]] Texture2D normalMap; +[[vk::binding(3, 0)]] SamplerState normalSampler; +[[vk::binding(4, 0)]] Texture2D occlusionMap; +[[vk::binding(4, 0)]] SamplerState occlusionSampler; +[[vk::binding(5, 0)]] Texture2D emissiveMap; +[[vk::binding(5, 0)]] SamplerState emissiveSampler; + +[[vk::push_constant]] PushConstants material; +---- + +**Key Concepts Explained:** + +The vertex input layout defines how vertex data is structured in GPU memory, with semantic annotations like POSITION and NORMAL telling the GPU how to interpret each data component. This structured approach allows the graphics pipeline to efficiently process vertex attributes and pass them through the rendering stages. + +When it comes to data management, we use two primary mechanisms: uniform buffers and push constants. Uniform buffers are larger, read-only memory blocks that efficiently store data shared across many draw calls, making them perfect for transformation matrices and lighting information that remain constant across multiple objects. Push constants, on the other hand, are smaller (typically limited to 128 bytes or less) but much faster for frequently changing per-object data like material properties, making them ideal for our material system. + +The resource binding syntax using `pass:[[[vk::binding(x, y)]]]` creates the essential link between CPU resources and GPU shader registers. The first number represents the binding index, while the second specifies the descriptor set, allowing us to organize and efficiently access textures, samplers, and other resources from within our shaders. + +Finally, the interpolation system works seamlessly in the background, where data in our `VSOutput` structure gets automatically interpolated across triangle surfaces by the GPU's rasterization hardware, ensuring smooth transitions of attributes like normals and texture coordinates across the rendered surface. + +=== Section 2: Helper Functions - PBR Mathematics + +This section contains the mathematical foundation of Physically Based Rendering. These functions implement the Cook-Torrance microfacet BRDF model, which approximates how light interacts with real-world materials at a microscopic level. + +[source,slang] +---- +// Normal Distribution Function (D) - GGX/Trowbridge-Reitz Distribution +// Describes the statistical distribution of microfacet orientations +float DistributionGGX(float NdotH, float roughness) { + float a = roughness * roughness; // Remapping for more perceptual linearity + float a2 = a * a; + float NdotH2 = NdotH * NdotH; + + float nom = a2; // Numerator: concentration factor + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; // Normalization factor + + return nom / denom; // Normalized distribution +} + +// Geometry Function (G) - Smith's method with Schlick-GGX approximation +// Models self-shadowing and masking between microfacets +float GeometrySmith(float NdotV, float NdotL, float roughness) { + float r = roughness + 1.0; + float k = (r * r) / 8.0; // Direct lighting remapping + + // Geometry obstruction from view direction (masking) + float ggx1 = NdotV / (NdotV * (1.0 - k) + k); + // Geometry obstruction from light direction (shadowing) + float ggx2 = NdotL / (NdotL * (1.0 - k) + k); + + return ggx1 * ggx2; // Combined masking-shadowing +} + +// Fresnel Reflectance (F) - Schlick's approximation +// Models how reflectance changes with viewing angle +float3 FresnelSchlick(float cosTheta, float3 F0) { + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} +---- + +**Mathematical Concepts & References:** + +The foundation of our PBR implementation rests on microfacet theory, which recognizes that real surfaces consist of countless microscopic facets with varying orientations. Rather than trying to model each individual facet, the BRDF statistically represents their collective behavior, allowing us to achieve realistic lighting without the computational complexity of simulating every surface detail. This approach was thoroughly explored in Walter et al.'s seminal 2007 paper "Microfacet Models for Refraction through Rough Surfaces," which you can find at link:https://www.graphics.cornell.edu/~bjw/microfacetbsdf.pdf[their comprehensive BSDF documentation]. + +Our choice of the GGX distribution function, also known as Trowbridge-Reitz, stems from its ability to produce realistic highlight shapes with longer tails compared to older models like Blinn-Phong. This distribution function has become the standard in modern real-time rendering because it closely matches measured material data and provides the natural falloff that we observe in real-world materials. Eric Heitz's 2014 work "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs" provides deep insights into why this distribution works so well in practice. + +The Smith geometry function plays a crucial role by accounting for the statistical correlation between masking (when the viewer can't see a microfacet) and shadowing (when light can't reach a microfacet). This might seem like a technical detail, but it prevents energy gain at grazing angles where naive models become unrealistically bright, ensuring our materials look believable under all viewing conditions. + +The Fresnel effect captures a phenomenon we see every day: materials become more reflective at grazing angles, like water appearing mirror-like when viewed from the side. Schlick's approximation gives us this essential behavior while trading some accuracy for the performance we need in real-time applications. The F0 parameter represents reflectance at normal incidence (0° viewing angle), allowing us to control how reflective different materials appear when viewed head-on. + +Finally, energy conservation ensures that the sum of reflected and transmitted light never exceeds the incident light, maintaining physical plausibility. This principle guides how we balance diffuse and specular components, ensuring our materials look consistent and believable under varying lighting conditions. + +**Further Reading:** + +For deeper exploration of these concepts, "Real-Time Rendering, 4th Edition" Chapter 9 on Physically Based Shading provides comprehensive coverage of the theory and practice. The online "PBR Book" by Pharr, Jakob, and Humphreys at https://pbr-book.org/ offers an exhaustive mathematical treatment of physically based rendering. For practical implementation insights, Epic Games' "Real Shading in Unreal Engine 4" presentation from the link:https://blog.selfshadow.com/publications/s2013-shading-course/[2013 Shading Course] demonstrates how these concepts translate into production-ready code. + +=== Section 3: Vertex and Fragment Shader Main Bodies + +This section contains the actual shader entry points that execute for each vertex and fragment (pixel). The vertex shader transforms geometry, while the fragment shader implements the full PBR lighting model. + +[source,slang] +---- +// Vertex shader entry point - Executes once per vertex +[[shader("vertex")]] +VSOutput VSMain(VSInput input) +{ + VSOutput output; + + // Transform vertex position through the rendering pipeline + // Model -> World -> Camera -> Clip space transformation chain + float4 worldPos = mul(ubo.model, float4(input.Position, 1.0)); + output.Position = mul(ubo.proj, mul(ubo.view, worldPos)); + + // Pass world position for fragment lighting calculations + // Fragment shader needs world space position to calculate light vectors + output.WorldPos = worldPos.xyz; + + // Transform normal from model space to world space + // Use only rotation/scale part of model matrix (upper-left 3x3) + // Normalize to ensure unit length after transformation + output.Normal = normalize(mul((float3x3)ubo.model, input.Normal)); + + // Pass through texture coordinates unchanged + // UV coordinates are typically in [0,1] range and don't need transformation + output.UV = input.UV; + + // Pass tangent vector for normal mapping + // Will be used in fragment shader to construct tangent-space basis + output.Tangent = input.Tangent; + + return output; +} + +// Fragment shader entry point - Executes once per pixel +[[shader("fragment")]] +float4 PSMain(VSOutput input) : SV_TARGET +{ + // === MATERIAL PROPERTY SAMPLING === + // Sample base color texture and apply material color factor + float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; + + // Sample metallic-roughness texture (metallic=B channel, roughness=G channel) + // glTF standard: metallic stored in blue, roughness in green + float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; + float metallic = metallicRoughness.x * material.metallicFactor; + float roughness = metallicRoughness.y * material.roughnessFactor; + + // Sample ambient occlusion (typically stored in red channel) + float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; + + // Sample emissive texture for self-illuminating materials + float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; + + // === NORMAL CALCULATION === + // Start with interpolated surface normal + float3 N = normalize(input.Normal); + + // Apply normal mapping if texture is available + if (material.normalTextureSet >= 0) { + // Sample normal map and convert from [0,1] to [-1,1] range + float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; + + // Construct tangent-space to world-space transformation matrix (TBN) + float3 T = normalize(input.Tangent.xyz); // Tangent + float3 B = normalize(cross(N, T)) * input.Tangent.w; // Bitangent (w = handedness) + float3x3 TBN = float3x3(T, B, N); // Tangent-Bitangent-Normal matrix + + // Transform normal from tangent space to world space + N = normalize(mul(tangentNormal, TBN)); + } + + // === LIGHTING SETUP === + // Calculate view direction (camera to fragment) + float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + + // Calculate reflection vector for environment mapping + float3 R = reflect(-V, N); + + // === PBR MATERIAL SETUP === + // Calculate F0 (reflectance at normal incidence) + // Non-metals: low reflectance (~0.04), Metals: colored reflectance from base color + float3 F0 = float3(0.04, 0.04, 0.04); // Dielectric default + F0 = lerp(F0, baseColor.rgb, metallic); // Lerp to metallic behavior + + // Initialize outgoing radiance accumulator + float3 Lo = float3(0.0, 0.0, 0.0); + + // === DIRECT LIGHTING LOOP === + // Calculate contribution from each light source + for (int i = 0; i < 4; i++) { + float3 lightPos = ubo.lightPositions[i].xyz; + float3 lightColor = ubo.lightColors[i].rgb; + + // Calculate light direction and attenuation + float3 L = normalize(lightPos - input.WorldPos); // Light direction + float distance = length(lightPos - input.WorldPos); // Distance for falloff + float attenuation = 1.0 / (distance * distance); // Inverse square falloff + float3 radiance = lightColor * attenuation; // Attenuated light color + + // Calculate half vector (between view and light directions) + float3 H = normalize(V + L); + + // === BRDF EVALUATION === + // Calculate all necessary dot products for BRDF terms + float NdotL = max(dot(N, L), 0.0); // Lambertian falloff + float NdotV = max(dot(N, V), 0.0); // View angle + float NdotH = max(dot(N, H), 0.0); // Half vector for specular + float HdotV = max(dot(H, V), 0.0); // For Fresnel calculation + + // Evaluate Cook-Torrance BRDF components + float D = DistributionGGX(NdotH, roughness); // Normal distribution + float G = GeometrySmith(NdotV, NdotL, roughness); // Geometry function + float3 F = FresnelSchlick(HdotV, F0); // Fresnel reflectance + + // Calculate specular BRDF + float3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; // Prevent division by zero + float3 specular = numerator / denominator; + + // === ENERGY CONSERVATION === + // Fresnel term represents specular reflection ratio + float3 kS = F; // Specular contribution + float3 kD = float3(1.0, 1.0, 1.0) - kS; // Diffuse contribution (energy conservation) + kD *= 1.0 - metallic; // Metals have no diffuse reflection + + // === RADIANCE ACCUMULATION === + // Combine diffuse (Lambertian) and specular (Cook-Torrance) terms + // Multiply by incident radiance and cosine foreshortening + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + } + + // === AMBIENT AND EMISSIVE === + // Add simple ambient lighting (should be replaced with IBL in production) + float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; + + // Combine all lighting contributions + float3 color = ambient + Lo + emissive; + + // === HDR TONE MAPPING AND GAMMA CORRECTION === + // Apply Reinhard tone mapping to compress HDR values to [0,1] range + color = color / (color + float3(1.0, 1.0, 1.0)); + + // Apply gamma correction for sRGB display (inverse gamma) + color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + + // Output final color with original alpha + return float4(color, baseColor.a); +} +---- + +**Vertex Shader Objectives:** + +The vertex shader serves as the first stage of our rendering pipeline, with its primary responsibility being geometric transformation. It converts vertex positions through the standard MVP (Model-View-Projection) matrix pipeline, systematically transforming coordinates from model space to world space, then to camera space, and finally to clip space in preparation for rasterization. This transformation chain ensures that our 3D geometry appears correctly positioned and projected for the viewer. + +Beyond basic transformation, the vertex shader handles crucial attribute processing by transforming normals from model space to world space and passing through texture coordinates and tangent vectors that the fragment shader will need. This attribute processing ensures that lighting calculations in the fragment shader receive properly transformed surface information, while texture coordinates and tangent vectors maintain their relationships for accurate material sampling and normal mapping. + +The vertex shader also performs essential data preparation by setting up interpolated values that the fragment shader requires for lighting calculations. These interpolated values, such as world positions and transformed normals, get automatically interpolated across triangle surfaces by the GPU's rasterization hardware, providing smooth transitions that enable realistic per-pixel lighting in the subsequent fragment stage. + +**Fragment Shader Objectives:** + +The fragment shader represents the heart of our PBR implementation, beginning with comprehensive material sampling that extracts surface properties like color, roughness, and metallic values from texture maps. This sampling process reads multiple texture channels according to the glTF standard, combining texture data with material parameters passed through push constants to determine the final surface characteristics for each pixel. + +Normal mapping reconstruction forms another critical objective, where the fragment shader takes encoded normal information from normal maps and reconstructs detailed surface normals that simulate fine geometric detail without requiring additional geometry. This process involves sampling the normal map, transforming the values from texture space to world space using the tangent-bitangent-normal matrix, and applying the resulting detailed normals to lighting calculations. + +The core PBR lighting implementation brings together all these elements using the Cook-Torrance microfacet model with proper energy conservation. This involves evaluating the distribution, geometry, and Fresnel terms of the BRDF, carefully balancing diffuse and specular contributions to ensure physically plausible results across all viewing angles and material types. + +Finally, post-processing operations convert the HDR linear lighting results into display-appropriate sRGB values through tone mapping and gamma correction. This final stage compresses the high dynamic range values generated by realistic lighting calculations into the limited range that displays can show, while maintaining visual fidelity and preventing the harsh clipping that would otherwise occur with bright highlights. + +**Key Implementation Details:** + +Our implementation carefully follows established conventions and best practices to ensure compatibility and visual quality. We adhere to the glTF texture channel convention where metallic information uses the blue channel and roughness uses the green channel, enabling seamless integration with standard 3D authoring tools and asset pipelines. This convention ensures that materials created in external tools will render correctly without requiring texture channel remapping or custom import procedures. + +Energy conservation remains paramount throughout our implementation, with careful attention paid to ensuring that diffuse plus specular contributions never exceed unity through the kS/kD relationship. This physical constraint prevents materials from appearing to emit more light than they receive, maintaining believable appearance across different lighting conditions and viewing angles while avoiding the artificial brightness that can plague non-physically-based approaches. + +Numerical stability considerations appear throughout the implementation, with small epsilon values added to prevent division by zero in BRDF calculations and careful handling of edge cases where mathematical operations might produce undefined results. These seemingly minor details prove crucial for robust rendering that handles extreme material parameters and unusual viewing angles without producing artifacts or rendering failures. + +The HDR pipeline architecture ensures that all lighting calculations occur in linear space, preserving the full dynamic range of realistic lighting throughout the computation stages and only applying gamma correction at the final output stage. This approach maintains maximum precision and accuracy in the lighting calculations while ensuring that the final image appears correct on standard sRGB displays. + +This shader implements the PBR lighting model with the metallic-roughness workflow, but the goal here is not just to show "what" the code does — it's to explain "why" each piece exists. + +== Understanding the "Why" behind the shader + +=== Why these BRDF terms (D, G, F) + +The Normal Distribution Function (D) serves as the statistical heart of our microfacet model, determining how many surface microfacets are oriented to reflect light directly toward the viewer. This function explains why rough surfaces produce broader, dimmer highlights while smooth surfaces create tight, bright reflections. We chose the GGX distribution because it matches measured material data remarkably well and produces the natural long tails in highlights that we observe in real-world materials, avoiding the artificial cutoff that plagued older distribution functions like Blinn-Phong. + +The Geometry function (G) addresses a crucial physical reality: microfacets cast shadows on each other and can be hidden from view depending on the surface roughness and viewing angle. Without proper geometric consideration, highlights become unrealistically bright as roughness increases because we'd be ignoring the natural self-shadowing and masking that occurs on rough surfaces. Smith's approach with our roughness-derived k parameter provides an efficient yet physically plausible solution that maintains energy conservation across all viewing conditions. + +Fresnel reflectance (F) captures one of the most fundamental optical phenomena we encounter daily: surfaces become more reflective at grazing angles, just as you can see your reflection clearly in water when looking across its surface but hardly at all when looking straight down. Schlick's approximation gives us this essential angle-dependent behavior with minimal computational cost, while the F0 parameter allows us to control how reflective materials appear when viewed head-on, distinguishing between different material types. + +Energy conservation ties these components together by ensuring that the sum of reflected light never exceeds the incident light, maintaining physical plausibility. When more light reflects specularly (kS), correspondingly less can reflect diffusely (kD = 1 - kS), creating the natural balance that keeps materials looking believable across different lighting conditions and viewing angles while preventing the artificial brightness that can make rendered scenes look unrealistic. + +=== Why the metallic-roughness + +The metallic-roughness workflow has become the industry standard primarily due to its adoption by the glTF specification, which standardizes this approach with metallic information stored in the blue channel and roughness in the green channel by convention. This standardization creates a seamless ecosystem where assets created in any glTF-compliant tool will render consistently across different engines and applications, eliminating the texture channel confusion that plagued earlier workflows and enabling true asset interoperability. + +From an artistic perspective, this workflow proves remarkably intuitive because it presents artists with just two conceptual dials to control: metalness (distinguishing between non-metals and metals) and roughness (controlling the surface finish from perfectly smooth to completely rough), plus the base color. This simplification allows artists to focus on the visual intent rather than getting lost in complex parameter interactions, while still providing the full range of material appearances found in the real world. + +The workflow also handles F0 behavior correctly by encoding the fundamental difference between metallic and non-metallic materials. Non-metals typically have low F0 values around 0.02 to 0.08 (we use 0.04 as a reasonable default), while metals derive their colored specular reflectance directly from the base color. Our lerp(F0, baseColor, metallic) operation elegantly encodes this physical distinction, automatically transitioning from the achromatic reflectance of dielectrics to the colored reflectance of conductors as the metallic parameter increases. + +=== Why normal, occlusion, and emissive maps +Normal mapping represents one of the most powerful techniques in modern real-time rendering, allowing us to add high-frequency surface detail without increasing geometric complexity. By storing surface perturbations as RGB values in a texture, we can simulate fine details like scratches, rivets, or fabric weaves that would be prohibitively expensive to model with actual geometry. The magic happens in tangent space, where we reconstruct the perturbed normal vector N from the tangent-bitangent-normal (TBN) matrix, ensuring that lighting calculations respond to these small-scale surface features as if they were real geometric details. +- Ambient occlusion (AO): Dampens indirect light in crevices the global model doesn’t capture. We multiply the ambient/IBL term by AO to avoid overly flat shading. +- Emissive: Lets materials glow independent of lighting (e.g., LEDs, screens) and contributes additively so it’s visible even in darkness. + +=== Why HDR, exposure, and tone mapping +- Realistic light intensities create values far beyond [0,1] (e.g., sunlit surfaces, bright emitters). If we write those directly to an 8-bit display, they clip at 1.0, crushing detail and producing ugly, step-like highlights. +- Working in HDR (linear float) preserves detail through the lighting pipeline. Only at the end do we compress dynamic range using a tone mapper to fit the display. +- In this chapter we use simple Reinhard: color / (color + 1). It’s robust and artifact-free, good as a baseline. Alternatives you might adopt later: + * ACES (RRT/ODT): Filmic with good color preservation across extremes; widely used. + * Hable/Uncharted2 (“Filmic”): Nice highlight roll-off, tunable via curve parameters. + * Reinhard with exposure: Multiply color by an exposure before compressing to shift middle gray. +- Exposure parameter (ubo.exposure): Conceptually shifts scene brightness so midtones sit well under your chosen tone mapper. Even if the snippet shows a fixed operator, you can pre-scale color by exposure to support dynamic auto-exposure. +- Gamma correction (ubo.gamma): Displays are non-linear (approx 2.2). Lighting must happen in linear space, then we apply pow(color, 1/gamma) right before writing to the sRGB framebuffer. Skipping this causes washed-out or too-dark images. +- Pipeline note: Prefer sRGB formats for color attachments when presenting. If writing to an sRGB swapchain image, do gamma in shader OR use sRGB formats so hardware handles it — not both. Do exactly one. + +=== Practical tuning checklist +- If highlights look “plasticky” everywhere, roughness may be too low or kD not reduced by metallic; verify kD *= (1 - metallic). +- If everything clips to white, add/adjust exposure and switch to ACES or Filmic tone mapping. +- If colors shift in highlights, check that tone mapping happens in linear space and gamma is applied only once. +- If normal maps look inverted or seams appear, verify tangent handedness (TBN), normal map channel order, and normal map space. +- If ambient looks flat, confirm AO is applied to ambient/IBL but not to direct specular. + +== Extending the Renderer + +Now that we have our PBR shader, we need to extend our renderer to support it. We'll need to: + +1. Add a new pipeline for our PBR shader +2. Add support for push constants +3. Update the uniform buffer to include light information + +Let's start by adding a new function to create the PBR pipeline. This process involves several distinct steps, each serving a specific purpose in configuring the Vulkan graphics pipeline for physically based rendering. + +=== Shader Module Creation and Stage Setup + +First, we load our compiled shader and set up the programmable stages of the graphics pipeline. Vulkan requires us to explicitly specify which shader stages we'll use and their entry points. + +[source,cpp] +---- +bool Renderer::createPBRPipeline() { + try { + // Load our compiled PBR shader from disk + // The .spv file contains both vertex and fragment shader code compiled by slangc + auto shaderCode = readFile("shaders/pbr.spv"); + + // Create a shader module - this is Vulkan's container for shader bytecode + // The shader module acts as a wrapper around the SPIR-V bytecode that GPU drivers understand + vk::raii::ShaderModule shaderModule = createShaderModule(shaderCode); + + // Configure the vertex shader stage + // This tells Vulkan which shader stage this module serves and its entry point function + vk::PipelineShaderStageCreateInfo vertShaderStageInfo; + vertShaderStageInfo.setStage(vk::ShaderStageFlagBits::eVertex) + .setModule(*shaderModule) + .setPName("VSMain"); // Must match the vertex shader function name + + // Configure the fragment shader stage + // Same module, different entry point - this is how combined shaders work + vk::PipelineShaderStageCreateInfo fragShaderStageInfo; + fragShaderStageInfo.setStage(vk::ShaderStageFlagBits::eFragment) + .setModule(*shaderModule) + .setPName("PSMain"); // Must match the fragment shader function name + + std::array shaderStages = {vertShaderStageInfo, fragShaderStageInfo}; +---- + +The entry point names ("VSMain" and "PSMain") must exactly match the function names in our shader code. This explicit binding system gives us fine-grained control over which functions serve which pipeline stages, and it's particularly useful when working with shader libraries that contain multiple variations of vertex or fragment shaders. + +=== Vertex Input Configuration + +The vertex input state defines how vertex data flows from our vertex buffers into the vertex shader. This configuration must precisely match the vertex format expected by our PBR shader. + +[source,cpp] +---- + // Configure how vertex data is structured and fed to the vertex shader + vk::PipelineVertexInputStateCreateInfo vertexInputInfo; + + // Define the vertex buffer binding - describes the overall vertex structure + // This tells Vulkan the total size of each vertex and how vertices are arranged + vk::VertexInputBindingDescription bindingDescription; + bindingDescription.setBinding(0) // Binding point 0 + .setStride(sizeof(float) * 14) // Total vertex size: pos(3) + normal(3) + uv(2) + tangent(4) + bitangent(2) + .setInputRate(vk::VertexInputRate::eVertex); // Data advances per vertex (not per instance) + + // Define individual vertex attributes - each corresponds to an input in our vertex shader + std::array attributeDescriptions; + + // Position attribute: 3D coordinates in model space + attributeDescriptions[0].setBinding(0) // From binding 0 + .setLocation(0) // Shader input location 0 + .setFormat(vk::Format::eR32G32B32Sfloat) // Three 32-bit floats (RGB) + .setOffset(0); // Start of vertex data + + // Normal attribute: surface normal for lighting calculations + attributeDescriptions[1].setBinding(0) + .setLocation(1) // Shader input location 1 + .setFormat(vk::Format::eR32G32B32Sfloat) + .setOffset(sizeof(float) * 3); // After position + + // Texture coordinate attribute: UV mapping coordinates + attributeDescriptions[2].setBinding(0) + .setLocation(2) // Shader input location 2 + .setFormat(vk::Format::eR32G32Sfloat) // Two 32-bit floats (RG) + .setOffset(sizeof(float) * 6); // After position + normal + + // Tangent attribute: tangent vector for normal mapping (includes handedness in W) + attributeDescriptions[3].setBinding(0) + .setLocation(3) // Shader input location 3 + .setFormat(vk::Format::eR32G32B32A32Sfloat) // Four 32-bit floats (RGBA) + .setOffset(sizeof(float) * 8); // After position + normal + UV + + // Bitangent attribute: completes the tangent space basis + attributeDescriptions[4].setBinding(0) + .setLocation(4) // Shader input location 4 + .setFormat(vk::Format::eR32G32Sfloat) + .setOffset(sizeof(float) * 12); // After all previous attributes + + // Connect the binding and attribute descriptions to the vertex input state + vertexInputInfo.setVertexBindingDescriptionCount(1) + .setPVertexBindingDescriptions(&bindingDescription) + .setVertexAttributeDescriptionCount(static_cast(attributeDescriptions.size())) + .setPVertexAttributeDescriptions(attributeDescriptions.data()); +---- + +The vertex input configuration serves as a contract between our vertex buffer data and the vertex shader inputs. Each attribute description maps a specific piece of vertex data to a shader input location, with precise format and offset specifications. This explicit mapping system ensures that the GPU correctly interprets our vertex data regardless of how it's packed in memory. + +The stride calculation (14 floats) reflects our comprehensive vertex format that supports full PBR rendering: position for geometry, normals for basic lighting, UV coordinates for texture sampling, and tangent vectors for normal mapping. The tangent vector includes a fourth component (W) that stores handedness information, which is crucial for correctly reconstructing the bitangent vector in cases where the tangent space might be flipped. + +The offset calculations ensure that each attribute starts at the correct byte position within each vertex. This precise alignment is for performance, as misaligned vertex data can cause significant performance penalties on some GPU architectures. + +=== Input Assembly and Primitive Processing + +The input assembly stage determines how vertices are grouped into geometric primitives and how the GPU should interpret the vertex stream. + +[source,cpp] +---- + // Configure input assembly - how vertices become triangles + vk::PipelineInputAssemblyStateCreateInfo inputAssembly; + inputAssembly.setTopology(vk::PrimitiveTopology::eTriangleList) // Every 3 vertices form a triangle + .setPrimitiveRestartEnable(false); // Don't use primitive restart indices +---- + +Triangle lists represent the most straightforward and commonly used primitive topology for complex 3D models. In this mode, every group of three consecutive vertices defines a complete triangle, providing maximum flexibility for representing arbitrary geometry. While other topologies like triangle strips or fans can be more memory-efficient for certain geometric patterns, triangle lists avoid the complexity of degenerate triangles and vertex ordering constraints that can arise with more compact representations. + +Primitive restart functionality allows special index values to signal the end of one primitive and the beginning of another, but this feature adds complexity that's unnecessary for most PBR rendering scenarios. By disabling it, we ensure predictable behavior and avoid potential performance penalties associated with index buffer scanning. + +=== Viewport and Dynamic State Configuration + +The viewport state manages the transformation from normalized device coordinates to screen coordinates, while dynamic state configuration allows certain pipeline parameters to be changed without recreating the entire pipeline. + +[source,cpp] +---- + // Configure viewport and scissor state + // We'll set actual viewport and scissor rectangles dynamically at render time + vk::PipelineViewportStateCreateInfo viewportState; + viewportState.setViewportCount(1) // Single viewport (most common case) + .setScissorCount(1); // Single scissor rectangle + + // Define which pipeline state can be changed dynamically + // This improves performance by avoiding pipeline recreation for common changes + std::vector dynamicStates = { + vk::DynamicState::eViewport, // Viewport can change (window resize, camera changes) + vk::DynamicState::eScissor // Scissor rectangle can change (UI clipping, effects) + }; + + vk::PipelineDynamicStateCreateInfo dynamicState; + dynamicState.setDynamicStateCount(static_cast(dynamicStates.size())) + .setPDynamicStates(dynamicStates.data()); +---- + +Dynamic state configuration represents a key optimization in modern Vulkan applications. By marking viewport and scissor as dynamic, we avoid the expensive pipeline recreation that would otherwise be required for common operations like window resizing or camera adjustments. The GPU driver can efficiently update these parameters at command recording time rather than requiring a completely new pipeline state object. + +The single viewport approach covers the vast majority of rendering scenarios. Multi-viewport rendering is primarily used for specialized applications like VR stereo rendering or certain shadow mapping techniques, but single-viewport rendering provides optimal performance for standard PBR applications. + +=== Rasterization Configuration + +The rasterization stage converts geometric primitives into fragments (potential pixels) and applies various geometric processing options that affect how triangles are converted to pixels. + +[source,cpp] +---- + // Configure rasterization - how triangles become pixels + vk::PipelineRasterizationStateCreateInfo rasterizer; + rasterizer.setDepthClampEnable(false) // Don't clamp depth values (standard behavior) + .setRasterizerDiscardEnable(false) // Don't discard primitives before rasterization + .setPolygonMode(vk::PolygonMode::eFill) // Fill triangles (not wireframe or points) + .setLineWidth(1.0f) // Line width (only relevant for wireframe) + .setCullMode(vk::CullModeFlagBits::eBack) // Cull back-facing triangles + .setFrontFace(vk::FrontFace::eCounterClockwise) // Counter-clockwise vertices = front-facing + .setDepthBiasEnable(false); // No depth bias (used for shadow mapping) +---- + +The rasterization configuration directly impacts both rendering performance and visual quality. Back-face culling provides a significant performance boost by eliminating triangles that face away from the camera, effectively halving the fragment processing workload for typical closed meshes. The counter-clockwise winding order follows the standard convention used by most 3D modeling tools and asset pipelines. + +Fill mode produces solid triangles appropriate for PBR rendering, though wireframe mode can be useful for debugging geometry or creating special visual effects. The line width setting only affects wireframe rendering, but some graphics drivers require it to be specified even when using fill mode. + +Depth bias (also known as polygon offset) is commonly used in shadow mapping to prevent self-shadowing artifacts, but it's unnecessary for standard forward rendering and can introduce its own artifacts if used inappropriately. + +=== Multisampling and Anti-Aliasing + +The multisampling configuration determines how the GPU handles anti-aliasing to reduce visual artifacts from geometric edges. + +[source,cpp] +---- + // Configure multisampling - anti-aliasing settings + vk::PipelineMultisampleStateCreateInfo multisampling; + multisampling.setSampleShadingEnable(false) // Disable per-sample shading + .setRasterizationSamples(vk::SampleCountFlagBits::e1); // No multisampling (1 sample per pixel) +---- + +This configuration disables multisampling anti-aliasing (MSAA) for simplicity and performance. While MSAA can significantly improve visual quality by reducing aliasing artifacts on geometric edges, it also substantially increases memory bandwidth requirements and fragment processing costs. For learning purposes and initial implementations, single-sample rendering provides a good balance between performance and complexity. + +In production applications, you might enable MSAA by increasing the sample count to 4x or 8x, depending on performance requirements and target hardware capabilities. Per-sample shading, when enabled, runs the fragment shader once per sample rather than once per pixel, providing the highest quality anti-aliasing at the cost of proportionally increased fragment processing time. + +=== Phase 7: Depth Testing and Z-Buffer Configuration + +The depth and stencil state configuration controls how fragments interact with the depth buffer to achieve proper depth sorting and occlusion. + +[source,cpp] +---- + // Configure depth and stencil testing + vk::PipelineDepthStencilStateCreateInfo depthStencil; + depthStencil.setDepthTestEnable(true) // Enable depth testing for proper occlusion + .setDepthWriteEnable(true) // Write depth values to depth buffer + .setDepthCompareOp(vk::CompareOp::eLess) // Fragment passes if its depth is less (closer) + .setDepthBoundsTestEnable(false) // Don't use depth bounds testing + .setStencilTestEnable(false); // Don't use stencil testing +---- + +Depth testing forms the foundation of proper 3D rendering by ensuring that closer objects occlude more distant ones. The "less than" comparison function works with the standard depth buffer convention where smaller depth values represent closer fragments. This configuration writes depth values for each rendered fragment, building up the depth buffer that subsequent draw calls can use for occlusion testing. + +Depth bounds testing and stencil testing are advanced features used for specific rendering techniques like light volume optimization or complex compositing operations. For standard PBR rendering, they add unnecessary complexity without providing benefits, so we disable them to maintain optimal performance. + +=== Phase 8: Color Blending and Transparency + +The color blend state determines how new fragments combine with existing color values in the framebuffer, enabling transparency and various compositing effects. + +[source,cpp] +---- + // Configure color blending - how new pixels combine with existing ones + vk::PipelineColorBlendAttachmentState colorBlendAttachment; + colorBlendAttachment.setColorWriteMask( + vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | // Write all color channels + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA) + .setBlendEnable(true) // Enable alpha blending + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) // New fragment's alpha + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) // One minus new fragment's alpha + .setColorBlendOp(vk::BlendOp::eAdd) // Add source and destination + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) // Preserve new alpha + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) // Ignore old alpha + .setAlphaBlendOp(vk::BlendOp::eAdd); // Add alpha values + + vk::PipelineColorBlendStateCreateInfo colorBlending; + colorBlending.setLogicOpEnable(false) // Don't use logical operations + .setAttachmentCount(1) // Single color attachment + .setPAttachments(&colorBlendAttachment); +---- + +This blend configuration implements standard alpha transparency using the classic "over" compositing operation. The formula `(srcAlpha * newColor) + ((1 - srcAlpha) * oldColor)` produces natural-looking transparency effects where fully opaque fragments (alpha = 1) completely replace the background, while partially transparent fragments blend proportionally. + +The separate alpha blending configuration preserves the alpha channel properly for potential multi-pass rendering or post-processing effects. By setting source alpha factor to one and destination alpha factor to zero, we ensure that the final alpha value comes entirely from the new fragment, which is typically the desired behavior for transparency effects. + +=== Phase 9: Pipeline Layout and Resource Binding + +The pipeline layout defines how resources like textures, uniform buffers, and push constants are organized and accessed by the shaders. + +[source,cpp] +---- + // Configure push constants for fast material property updates + vk::PushConstantRange pushConstantRange; + pushConstantRange.setStageFlags(vk::ShaderStageFlagBits::eFragment) // Only fragment shader uses these + .setOffset(0) // Start at beginning + .setSize(sizeof(PushConstantBlock)); // Size of our material data + + // Create the pipeline layout - defines resource organization + vk::PipelineLayoutCreateInfo pipelineLayoutInfo; + pipelineLayoutInfo.setSetLayoutCount(1) // Single descriptor set + .setPSetLayouts(&*descriptorSetLayout) // Our texture/uniform bindings + .setPushConstantRangeCount(1) // One push constant block + .setPPushConstantRanges(&pushConstantRange); + + // Create the pipeline layout object + pbrPipelineLayout = device.createPipelineLayout(pipelineLayoutInfo); +---- + +The pipeline layout serves as a contract between the application and shaders regarding resource organization. Push constants provide the fastest path for updating small amounts of data (like material properties) between draw calls, as they bypass the memory hierarchy and are directly accessible to shader cores. The 128-byte limit on push constants in most implementations makes them perfect for per-material data but unsuitable for larger datasets. + +The descriptor set layout reference connects our pipeline to the texture and uniform buffer bindings we established earlier. This separation of concerns allows the same descriptor set layout to be used across multiple pipelines while maintaining clean resource organization. + +=== Phase 10: Final Pipeline Creation and Dynamic Rendering Setup + +The final phase assembles all configuration states into a complete graphics pipeline and sets up dynamic rendering compatibility for modern Vulkan applications. + +[source,cpp] +---- + // Assemble the complete graphics pipeline + vk::GraphicsPipelineCreateInfo pipelineInfo; + pipelineInfo.setStageCount(static_cast(shaderStages.size())) // Number of shader stages + .setPStages(shaderStages.data()) // Shader stage configurations + .setPVertexInputState(&vertexInputInfo) // Vertex format + .setPInputAssemblyState(&inputAssembly) // Primitive topology + .setPViewportState(&viewportState) // Viewport configuration + .setPRasterizationState(&rasterizer) // Rasterization settings + .setPMultisampleState(&multisampling) // Anti-aliasing settings + .setPDepthStencilState(&depthStencil) // Depth/stencil testing + .setPColorBlendState(&colorBlending) // Blending configuration + .setPDynamicState(&dynamicState) // Dynamic state settings + .setLayout(*pbrPipelineLayout) // Resource layout + .setRenderPass(nullptr) // Using dynamic rendering + .setSubpass(0) // Subpass index + .setBasePipelineHandle(nullptr); // No base pipeline + + // Configure for dynamic rendering (modern Vulkan approach) + vk::PipelineRenderingCreateInfo renderingInfo; + renderingInfo.setColorAttachmentCount(1) // Single color target + .setPColorAttachmentFormats(&swapChainImageFormat) // Match swapchain format + .setDepthAttachmentFormat(findDepthFormat()); // Depth buffer format + pipelineInfo.setPNext(&renderingInfo); + + // Create the final graphics pipeline + pbrPipeline = device.createGraphicsPipeline(nullptr, pipelineInfo); + + return true; + } catch (const std::exception& e) { + std::cerr << "Error creating PBR pipeline: " << e.what() << std::endl; + return false; + } +} +---- + +The pipeline creation represents the culmination of all our configuration work, where Vulkan validates the entire pipeline specification and compiles it into an optimized form suitable for GPU execution. The dynamic rendering configuration replaces the traditional render pass system with a more flexible approach that allows render targets to be specified at command recording time rather than pipeline creation time. + +This flexibility proves particularly valuable for applications that need to render to different targets (like shadow maps, reflection textures, or post-processing buffers) using the same pipeline. The format specifications ensure that the pipeline generates output compatible with our target render surfaces. + +The exception handling provides essential feedback during development, as pipeline creation failures can result from subtle configuration mismatches or resource compatibility issues that are difficult to debug without proper error reporting. + +This function creates a new pipeline for our PBR shader, including support for push constants. We'll also need to update our uniform buffer to include light information: + +[source,cpp] +---- +// Update uniform buffer +void Renderer::updateUniformBuffer(uint32_t currentFrame, Entity* entity, CameraComponent* camera) { + // Get the transform component from the entity + auto transform = entity->GetComponent(); + if (!transform) { + std::cerr << "Entity does not have a transform component" << std::endl; + return; + } + + // Create the uniform buffer object + UniformBufferObject ubo{}; + + // Set the model matrix from the entity's transform + ubo.model = transform->GetModelMatrix(); + + // Set the view and projection matrices from the camera + if (camera) { + ubo.view = camera->GetViewMatrix(); + ubo.proj = camera->GetProjectionMatrix(); + } else { + // Default view and projection matrices if no camera is provided + ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); + ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float)swapChainExtent.height, 0.1f, 100.0f); + ubo.proj[1][1] *= -1; // Flip Y coordinate for Vulkan + } + + // Set up lights + // Light 1: White light from above + ubo.lightPositions[0] = glm::vec4(0.0f, 5.0f, 5.0f, 1.0f); + ubo.lightColors[0] = glm::vec4(300.0f, 300.0f, 300.0f, 1.0f); + + // Light 2: Blue light from the left + ubo.lightPositions[1] = glm::vec4(-5.0f, 0.0f, 0.0f, 1.0f); + ubo.lightColors[1] = glm::vec4(0.0f, 0.0f, 300.0f, 1.0f); + + // Light 3: Red light from the right + ubo.lightPositions[2] = glm::vec4(5.0f, 0.0f, 0.0f, 1.0f); + ubo.lightColors[2] = glm::vec4(300.0f, 0.0f, 0.0f, 1.0f); + + // Light 4: Green light from behind + ubo.lightPositions[3] = glm::vec4(0.0f, -5.0f, 0.0f, 1.0f); + ubo.lightColors[3] = glm::vec4(0.0f, 300.0f, 0.0f, 1.0f); + + // Set camera position for view-dependent effects + ubo.camPos = glm::vec4(camera ? camera->GetPosition() : glm::vec3(2.0f, 2.0f, 2.0f), 1.0f); + + // Set PBR parameters + ubo.exposure = 4.5f; + ubo.gamma = 2.2f; + ubo.prefilteredCubeMipLevels = 1.0f; + ubo.scaleIBLAmbient = 1.0f; + + // Copy the uniform buffer object to the device memory using vk::raii + // With vk::raii, we can use the mapped memory directly + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); +} +---- + +Finally, we need to add support for pushing material properties to the shader: + +[source,cpp] +---- +// Push material properties to shader +void Renderer::pushMaterialProperties(vk::CommandBuffer commandBuffer, const Model* model, uint32_t materialIndex) { + // Get material from the model + const Material& material = model->materials[materialIndex]; + + // Define push constants + PushConstantBlock pushConstants{}; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex; + pushConstants.normalTextureSet = material.normalTextureIndex; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex; + pushConstants.alphaMask = material.alphaMode == AlphaMode::MASK ? 1.0f : 0.0f; + pushConstants.alphaMaskCutoff = material.alphaCutoff; + + // Push constants to shader using vk::raii + commandBuffer.pushConstants( + *pbrPipelineLayout, + vk::ShaderStageFlagBits::eFragment, + 0, + sizeof(PushConstantBlock), + &pushConstants + ); +} +---- + +In the next section, we'll integrate our lighting implementation with the rest of the Vulkan rendering pipeline. + +link:03_push_constants.adoc[Previous: Push Constants] | link:05_vulkan_integration.adoc[Next: Vulkan Integration] 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 new file mode 100644 index 00000000..c11378cc --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/05_vulkan_integration.adoc @@ -0,0 +1,221 @@ += Vulkan Integration + +In this section, we'll integrate our PBR implementation with the rest of the Vulkan rendering pipeline. We'll update our renderer class to support advanced lighting techniques that can be used with glTF models and their PBR materials. The techniques we develop here will be applied in the Loading_Models chapter when we load and render glTF models. + +To keep the flow concrete and avoid repeating earlier theory, use this quick roadmap: + +1) Extend the renderer with PBR pipeline objects and a material push-constant block +2) Create the PBR pipeline (layout, shaders, blending, formats) alongside the main pipeline +3) Record draws: bind PBR pipeline, bind geometry, and push per-material constants per mesh +4) Clean up via RAII (no special teardown required) + +[NOTE] +==== +We won’t re-explain PBR theory or push-constant fundamentals here. See Lighting_Materials/03_push_constants.adoc for push constants and Lighting_Materials/01_introduction.adoc (and 05_pbr_rendering.adoc) for PBR concepts. +==== + +The PBR pass slots into the graphics pipeline as shown below: + +image::../../../images/rendering_pipeline_flowchart.png[Rendering Pipeline Flowchart, width=600, alt=Rendering pipeline flowchart showing where the PBR pass fits] + +== Updating the Renderer Class + +First, let's update our renderer class to include the new members we need for our PBR implementation: + +[source,cpp] +---- +class Renderer { +public: + // ... existing members ... + + // PBR pipeline + vk::raii::PipelineLayout pbrPipelineLayout; + vk::raii::Pipeline pbrPipeline; + + // Push constant block for PBR material properties + struct PushConstantBlock { + glm::vec4 baseColorFactor; + float metallicFactor; + float roughnessFactor; + int baseColorTextureSet; + int physicalDescriptorTextureSet; + int normalTextureSet; + int occlusionTextureSet; + int emissiveTextureSet; + float alphaMask; + float alphaMaskCutoff; + }; + + // ... existing methods ... + + // New methods + bool createPBRPipeline(); + void pushMaterialProperties(vk::CommandBuffer commandBuffer, const Model* model, uint32_t materialIndex); +}; +---- + +We've added members for the PBR pipeline and a struct for PBR material properties. We've also added methods for creating the PBR pipeline and pushing material properties to the shader. + +== Updating the Initialization + +Next, we need to update the initialization process to create our PBR pipeline: + +[source,cpp] +---- +bool Renderer::Initialize(const std::string& appName, bool enableValidationLayers) { + // ... existing initialization code ... + + // Create graphics pipeline + if (!createGraphicsPipeline()) { + return false; + } + + // Create PBR pipeline + if (!createPBRPipeline()) { + std::cerr << "Failed to create PBR pipeline" << std::endl; + return false; + } + + // ... rest of initialization code ... + + initialized = true; + return true; +} +---- + +== Updating the Cleanup + +We also need to update the cleanup process to destroy our PBR pipeline: + +[source,cpp] +---- +void Renderer::Cleanup() { + // ... existing cleanup code ... + + // With vk::raii, pipeline and pipeline layout objects are automatically destroyed + // when they go out of scope, so we don't need explicit destruction calls + + // ... rest of cleanup code ... +} +---- + +== Updating the Rendering Process + +Finally, we need to update the rendering process to use our PBR pipeline and push material properties: + +[source,cpp] +---- +void Renderer::recordCommandBuffer(vk::CommandBuffer commandBuffer, uint32_t imageIndex) { + // ... existing command buffer recording code ... + + // Bind the PBR pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, *pbrPipeline); + + // For each model in the scene + for (const auto& model : models) { + // Bind vertex and index buffers + vk::Buffer vertexBuffers[] = {model->vertexBuffer}; + vk::DeviceSize offsets[] = {0}; + commandBuffer.bindVertexBuffers(0, 1, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(model->indexBuffer, 0, vk::IndexType::eUint32); + + // For each mesh in the model + for (const auto& mesh : model->meshes) { + // Push material properties + pushMaterialProperties(commandBuffer, model, mesh.materialIndex); + + // Bind descriptor sets + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + *pbrPipelineLayout, + 0, + 1, + &descriptorSets[imageIndex], + 0, + nullptr + ); + + // Draw + commandBuffer.drawIndexed(mesh.indexCount, 1, mesh.firstIndex, 0, 0); + } + } + + // ... rest of command buffer recording code ... +} +---- + +== PBR Shader Reference + +This chapter reuses the exact PBR shader defined in the previous section to avoid duplication and drift. Please refer to link:04_lighting_implementation.adoc[Implementing the PBR Shader] for the full pbr.slang source and detailed explanations. Here we focus strictly on Vulkan integration: pipeline layout, descriptor bindings, push constants, and draw submission. + + +== Compiling the Shader + +After creating the shader file, we need to compile it using slangc. This is typically done as part of the build process, but we can also do it manually: + +[source,bash] +---- +slangc shaders/pbr.slang -target spirv -profile spirv_1_4 -o shaders/pbr.spv +---- + +== Testing the Implementation with glTF Models + +To test our implementation, we can use glTF models, which already have PBR materials defined that are compatible with our implementation. In the Loading_Models chapter, we'll learn how to load these models, but for now, let's assume we have a way to load them. + +Here's an example of how to set up a test scene with glTF models: + +[source,cpp] +---- +void Renderer::renderTestScene() { + // Set up camera + glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); + glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f); + glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f); + + // Set up lights + // Light 1: White light from above + glm::vec4 lightPos1 = glm::vec4(0.0f, 5.0f, 5.0f, 1.0f); + glm::vec4 lightColor1 = glm::vec4(300.0f, 300.0f, 300.0f, 1.0f); + + // Light 2: Blue light from the left + glm::vec4 lightPos2 = glm::vec4(-5.0f, 0.0f, 0.0f, 1.0f); + glm::vec4 lightColor2 = glm::vec4(0.0f, 0.0f, 300.0f, 1.0f); + + // Load glTF models + Model* damagedHelmet = modelLoader.loadModel("models/DamagedHelmet/DamagedHelmet.gltf"); + Model* flightHelmet = modelLoader.loadModel("models/FlightHelmet/FlightHelmet.gltf"); + + // The models already have PBR materials defined in the glTF file + // We can render them directly with our PBR pipeline + + // Render the models with different transformations + renderModel(damagedHelmet, glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.5f)); + renderModel(flightHelmet, glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.5f)); + + // We can also experiment with modifying the material properties + // For example, to make the damaged helmet more metallic: + if (damagedHelmet->materials.size() > 0) { + // Store the original value to restore later + float originalMetallic = damagedHelmet->materials[0].metallicFactor; + + // Modify the material + damagedHelmet->materials[0].metallicFactor = 1.0f; + + // Render with modified material + renderModel(damagedHelmet, glm::vec3(-2.0f, 0.0f, 0.0f), glm::vec3(0.5f)); + + // Restore original value + damagedHelmet->materials[0].metallicFactor = originalMetallic; + } +} +---- + +== Conclusion + +In this section, we've integrated our PBR implementation with the rest of the Vulkan rendering pipeline. We've updated our renderer class to support advanced lighting techniques that can be used with glTF models and their PBR materials. We've created a PBR shader based on the concepts we've learned and shown how to test the implementation with glTF models. + +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. + +link:04_lighting_implementation.adoc[Previous: Lighting Implementation] | link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc new file mode 100644 index 00000000..c3e237b4 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/06_conclusion.adoc @@ -0,0 +1,44 @@ += 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. + +== 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. + +== Making it click: a mental model of this PBR pipeline + +At a high level, think of your frame as a linear-light computation that transforms physical inputs into displayable pixels: + +- Inputs in linear space: lights with intensities in physical-ish units, baseColor/metallic/roughness from material, and normal/AO/emissive maps. The work is done in linear HDR so you don’t lose headroom. +- BRDF roles: D shapes the highlight (roughness controls lobe width), G enforces masking/self-shadowing on microfacets, F boosts reflectance at grazing angles and ties reflectivity to material type via F0. Energy conservation links specular (kS) and diffuse (kD) so total doesn’t exceed what came in. +- Material knobs as levers: + * Roughness: widens/narrows the specular lobe and also reduces peak intensity via G. + * Metallic: cross-fades between dielectric behavior (colored diffuse + neutral specular) and conductor behavior (colored specular, no diffuse). + * Base color: is diffuse albedo for dielectrics and colored specular for metals. +- Normal/AO/emissive context: normal maps perturb local orientation to add detail, AO damps indirect/ambient to avoid flat crevices, emissive adds light-independent glow. +- Output staging: after summing ambient/indirect and direct lighting, compress HDR with a tone mapper (e.g., Reinhard/ACES) and only then apply gamma to match the display. Do gamma exactly once (either shader pow or sRGB framebuffer). + +A quick reasoning loop when results look off: + +1. Confirm spaces and order: linear lighting ➜ tone map ➜ gamma. Check you’re not doing double-gamma. +2. Probe the BRDF: plastic look on everything? Roughness too low or kD not reduced by metallic. Dim, muddy highlights? Roughness too high or exposure too low. +3. Validate normals/TBN: inverted green channel or wrong tangent handedness causes odd shading and seams. +4. Calibrate exposure/tone map: if whites clip harshly, add exposure control and/or switch to ACES/Hable for smoother roll-off. +5. Use AO and emissive judiciously: AO should affect ambient/IBL, not direct specular; emissive is additive and independent of lights. + +This mental model helps you predict how a change to any input will echo through the pipeline and appear on screen, which is the core of “understanding,” not just following a list. + +== 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. + +== Next Steps + +Pick one thread and go deep. For lighting, explore GI/AO/volumetrics as time allows. For materials, design a data‑driven system that maps glTF (and custom) parameters cleanly to your shaders. For visuals, prototype post effects (fog, bloom, DoF). For performance, profile first, then optimize the hot spots—especially on mobile. + +Remember that lighting is a complex topic with many approaches and techniques. The implementation we've covered in this chapter is just the beginning. As you continue to develop your engine, you'll likely want to refine and expand your lighting system to meet the specific needs of your projects. + +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] diff --git a/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc b/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc new file mode 100644 index 00000000..62d33ed2 --- /dev/null +++ b/en/Building_a_Simple_Engine/Lighting_Materials/index.adoc @@ -0,0 +1,14 @@ +:pp: {plus}{plus} + += Lighting & Materials: Basic lighting models and push constants + +This chapter covers the implementation of basic lighting models and the use of push constants for material properties in Vulkan. Throughout our engine implementation, we use vk::raii dynamic rendering and C++20 modules to create a modern, efficient, and maintainable codebase. + +== Contents + +* link:01_introduction.adoc[Introduction] +* link:02_lighting_models.adoc[Lighting Models] +* link:03_push_constants.adoc[Push Constants] +* link:04_lighting_implementation.adoc[Lighting Implementation] +* link:05_vulkan_integration.adoc[Vulkan Integration] +* link:06_conclusion.adoc[Conclusion] diff --git a/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc b/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc new file mode 100644 index 00000000..637c7177 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/01_introduction.adoc @@ -0,0 +1,33 @@ +:pp: {plus}{plus} + += Loading Models: Introduction + +== Introduction + +Welcome to the "Loading Models" chapter of our "Building a Simple Engine" series! After exploring engine architecture and camera systems in the previous chapters, we're now ready to focus on handling 3D assets within our engine framework. + +In this chapter, we'll set up a robust model loading system that can handle modern 3D assets. Building upon the engine architecture we've established and the camera system we've implemented, we'll now add the ability to load and render complex 3D models. In the link:../../15_GLTF_KTX2_Migration.html[chapter on glTF and KTX2] from the main tutorial, we learned about migrating from OBJ to glTF format and the basics of loading glTF models. Now, we'll integrate that knowledge into our engine structure to create a more complete implementation. + +This chapter will transform your understanding of 3D asset handling from simple model loading to sophisticated engine-level systems. We'll begin by building a scene graph, which provides the hierarchical organization that complex 3D scenes require. This foundation enables you to group objects logically, apply transformations at different levels, and efficiently manage scene complexity. + +Animation support forms a crucial part of modern 3D applications. We'll implement a system that can handle glTF's skeletal animations, giving life to your 3D models through smooth character movement, object animations, and complex multi-part systems. + +The PBR material system we'll create bridges the gap between the lighting concepts from previous chapters and real-world asset integration. You'll see how to map glTF material properties to your shaders seamlessly, creating a workflow that artists can understand and use effectively. + +Rendering multiple objects with different transformations presents both technical and organizational challenges. We'll solve these through careful engine architecture that can batch similar objects efficiently while maintaining the flexibility to handle unique materials and transformations per object. + +Throughout this implementation, we'll structure our code with engine-level thinking rather than tutorial-style solutions. This approach will serve you well as your projects grow in complexity and scope, providing a solid foundation for creating complex scenes with animated models. + +== Prerequisites + +This chapter builds on the foundation established in the main Vulkan tutorial, particularly Chapter 16 (Multiple Objects), as we'll extend those concepts to handle more complex scene organization and asset management. The multiple objects chapter introduced the basic concepts of rendering different geometry, which we'll now scale up to handle complete 3D models with materials and animations. + +You'll need solid familiarity with core Vulkan concepts that form the backbone of our model loading system. xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] become more complex when handling multiple models with different materials, as we'll need to manage descriptor sets and push constants efficiently. Understanding xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[graphics pipelines] is crucial since different materials might require different pipeline configurations. + +Experience with xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] translates directly to model loading, where glTF files contain vertex data in specific formats that we'll need to parse and upload to GPU buffers. xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] knowledge becomes essential as we'll use them for transformation matrices, lighting information, and material properties. + +xref:../../06_Texture_mapping/00_Images.adoc[Texture mapping] skills are particularly important since glTF models often include multiple textures per material (diffuse, normal, metallic-roughness, etc.), and we'll need to load and bind these textures efficiently. + +Finally, basic 3D math understanding (matrices, vectors, quaternions) is crucial for handling model transformations, animations, and scene hierarchies. If you need a refresher, see the xref:../../Building_a_Simple_Engine/Camera_Transformations/02_math_foundations.adoc[Camera Transformations chapter] for detailed coverage of these mathematical concepts. + +xref:../GUI/06_conclusion.adoc[Previous: GUI] | xref:02_project_setup.adoc[Next: Setting Up the Project] diff --git a/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc b/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc new file mode 100644 index 00000000..3f9efd9a --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/02_project_setup.adoc @@ -0,0 +1,375 @@ +::pp: {plus}{plus} + += Loading Models: Asset Pipeline Concepts +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Understanding Asset Pipelines + +After exploring engine architecture and camera systems, it's important to understand how 3D assets are managed in rendering engines. A well-designed asset pipeline is crucial for efficiently handling models, textures, and other resources in any production environment. + +=== Asset Organization Concepts + +When designing an asset organization system, consider these key principles: + +1. *Categorization* - Group similar assets together +2. *Hierarchy* - Use a nested structure to manage complexity +3. *Discoverability* - Make assets easy to find and reference +4. *Scalability* - Design for growth as your project expands + +Here's an example of how assets might be organized in a final product, demonstrating all four principles: + +[source] +---- +assets/ + ├── models/ // 3D model files (Categorization) + │ ├── characters/ // Character models (Hierarchy) + │ │ ├── player/ // Player character models (Hierarchy) + │ │ └── npc/ // Non-player character models (Hierarchy) + │ ├── environments/ // Environment models + │ │ ├── indoor/ // Indoor environment models + │ │ └── outdoor/ // Outdoor environment models + │ └── props/ // Prop models + ├── textures/ // Texture files (Categorization) + │ ├── common/ // Shared textures (Discoverability) + │ └── high_resolution/ // High-res textures for close-up views (Scalability) + ├── shaders/ // Shader files + │ ├── core/ // Essential shaders (Discoverability) + │ ├── effects/ // Special effect shaders + │ └── mobile/ // Mobile-optimized shaders (Scalability) + └── config/ // Configuration files + └── quality_presets/ // Different quality settings (Scalability) +---- + +This example demonstrates all four principles: +- *Categorization*: Assets are grouped by type (models, textures, shaders, config) +- *Hierarchy*: Assets are organized in a nested structure (e.g., models > characters > player) +- *Discoverability*: Common assets are placed in dedicated folders (e.g., common textures, core shaders) making them easy to find +- *Scalability*: The structure accommodates different quality levels and platform-specific assets (e.g., high-resolution textures, mobile shaders, quality presets) + +The specific organization should be tailored to your project's needs, but the underlying principles remain consistent across different engines. + +=== Asset Pipeline Concepts + +A professional asset pipeline typically involves several stages, regardless of the specific engine implementation: + +1. *Creation* - Artists create models in 3D modeling software +2. *Export* - Models are exported to interchange formats suitable for game engines +3. *Validation* - Models are checked for issues (e.g., incorrect scale, missing textures) +4. *Optimization* - Models are optimized for runtime performance +5. *Conversion* - Development assets are converted to production-ready formats +6. *Integration* - Assets are imported into the engine +7. *Runtime Loading* - The engine loads assets efficiently during execution + +When designing an asset pipeline, consider these important factors: + +==== File Format Selection + +Different file formats offer different trade-offs: + +1. *Interchange Formats* (e.g., glTF, FBX, Collada) + - Pros: Widely supported by modeling tools, preserve most data + - Cons: May contain unnecessary data, not optimized for runtime + +2. *Runtime Formats* (e.g., glb, engine-specific binary formats) + - Pros: Optimized for loading speed and memory usage + - Cons: May not be editable outside the engine + +==== Texture Compression + +Texture compression is crucial for performance: + +1. *Development Formats* (e.g., PNG, JPEG) + - Pros: Lossless or high quality, widely supported by editing tools + - Cons: Large file sizes, not optimized for GPU + +2. *Runtime Formats* (e.g., ktx, compressed GPU formats) + - Pros: Smaller file sizes, directly usable by GPU + - Cons: May have quality loss, platform-specific considerations + +==== Asset Bundling + +Consider how assets are packaged: + +1. *Separate Files* + - Pros: Easier to update individual assets, simpler version control + - Cons: More file operations, potential for missing dependencies + +2. *Bundled Assets* + - Pros: Fewer file operations, guaranteed dependencies + - Cons: Larger atomic updates, more complex version control + +=== Artist-Engine Collaboration Concepts + +Successful integration of art assets into a rendering engine requires clear communication and established workflows between artists and programmers. Here are key concepts to consider: + +==== Technical Specifications + +Regardless of the specific engine, you'll need to define: + +1. *Coordinate System* - Different applications use different coordinate systems (e.g., Y-up vs. Z-up) +2. *Scale* - Establish a consistent scale (e.g., 1 unit = 1 meter or 1 unit = 1 foot) +3. *Origin Placement* - Define where the origin point should be for different asset types +4. *Level of Detail* - Specify polygon count ranges for different asset types and usage scenarios + +==== Workflow Documentation + +Create documentation that addresses: + +1. *Naming Conventions* - Consistent naming helps with organization and automation +2. *Material Standards* - Define how materials should be structured (e.g., PBR parameters) +3. *Export Settings* - Document the correct export settings for your chosen interchange formats +4. *Quality Checklists* - Provide criteria for validating assets before submission + +==== Technical Art Bridge + +Consider establishing a technical art role that: + +1. Creates tools to streamline the art-to-engine pipeline +2. Validates assets before they enter the engine +3. Provides feedback to artists on technical requirements +4. Helps troubleshoot issues when assets don't appear correctly in-engine + +=== Development to Production Concepts + +The transition from artist-friendly development assets to optimized production assets involves several important concepts: + +==== Development vs. Production Assets + +Understanding the different needs at each stage: + +1. *Development Assets* + - Prioritize editability and iteration speed + - Use formats that are widely supported by content creation tools + - May be larger and less optimized for runtime performance + - Focus on preserving maximum quality and information + +2. *Production Assets* + - Prioritize runtime performance and memory efficiency + - Use formats optimized for the target platform(s) + - Apply appropriate compression and optimization techniques + - Balance quality against performance requirements + +==== Asset Validation + +Implement validation at key points in the pipeline: + +1. *Pre-Submission Validation* + - Check for adherence to technical specifications + - Verify that all required textures and materials are present + - Ensure proper scale, orientation, and origin placement + +2. *Pre-Conversion Validation* + - Verify that assets can be successfully processed by conversion tools + - Check for issues that might cause problems during conversion + +3. *Post-Conversion Validation* + - Verify that converted assets maintain visual fidelity + - Check for performance issues or memory consumption problems + - Ensure compatibility with target platforms + +==== Automation Considerations + +As projects grow, automation becomes increasingly important: + +1. *Batch Processing* + - Develop scripts or tools to process multiple assets at once + - Implement automated validation checks + +2. *Continuous Integration* + - Consider integrating asset processing into your CI/CD pipeline + - Automatically validate and convert assets when they're committed + +3. *Versioning* + - Track changes to assets and their processed versions + - Implement dependency tracking to rebuild only what's necessary + +=== Implementation Considerations + +When implementing a model loading system in any rendering engine, several key considerations should guide your approach: + +==== Abstraction Layers + +Design your model loading system with appropriate abstraction layers: + +1. *File Format Layer* + - Handles parsing specific file formats (e.g., glTF, FBX) + - Isolates format-specific code to make supporting multiple formats easier + - Converts from file format structures to your engine's internal structures + +2. *Resource Management Layer* + - Manages memory and GPU resources + - Handles caching and reference counting + - Provides a consistent interface regardless of the underlying file format + +3. *Scene Graph Layer* + - Organizes models in a hierarchical structure + - Manages transformations and parent-child relationships + - Facilitates operations like culling and scene traversal + +==== Performance Considerations + +Balance flexibility with performance: + +1. *Asynchronous Loading* + - Consider loading models in background threads to avoid blocking the main thread + - Implement a system for handling partially loaded models + +2. *Memory Management* + - Develop strategies for handling large models + - Consider level-of-detail (LOD) systems for complex scenes + - Implement streaming for very large environments + +3. *Batching and Instancing* + - Group similar models for efficient rendering + - Use instancing for repeated elements + +==== Extensibility + +Design for future expansion: + +1. *Material System* + - Create a flexible material system that can represent various shading models + - Support both simple and complex materials + +2. *Animation System* + - Design for different animation types (skeletal, morph targets, etc.) + - Consider how animations will interact with physics and gameplay systems + +3. *Custom Data* + - Allow for engine-specific metadata to be associated with models + - Support custom properties for gameplay or rendering purposes + +Understanding these concepts provides a solid foundation for designing and implementing model loading systems in any rendering engine. By carefully considering abstraction, performance, and extensibility from the beginning, you can create a robust system that will scale with your project's needs and adapt to changing requirements. + +== Our Project Implementation + +Now that we've explored the general concepts of asset pipelines, let's discuss how our specific project will implement these concepts. + +=== File Formats and Directory Structure + +For our engine, we'll use the following file formats and directory structure: + +1. *Model Format*: We'll use glTF 2.0 binary format (.glb) with embedded KTX2 textures. This format offers several advantages: + - Compact binary representation for efficient storage and loading + - Ability to embed textures, reducing file operations + - Support for animations, skinning, and PBR materials + - Industry standard with wide tool support + +2. *Texture Format*: We'll use KTX2 with Basis Universal compression for textures, which provides: + - Significant size reduction compared to PNG/JPEG + - GPU-ready formats that can be directly uploaded + - Cross-platform compatibility through transcoding + - Support for mipmaps and various compression formats + +3. *Directory Structure*: +[source] +---- +assets/ + ├── models/ // 3D model files + │ ├── characters/ // Character models + │ │ └── viking.glb // Example character model + │ ├── environments/ // Environment models + │ │ └── room.glb // Example environment model + │ └── props/ // Prop models + │ └── furniture.glb // Example prop model + └── shaders/ // Shader files + └── pbr.slang // PBR shader +---- + +=== Tools and Libraries + +We'll use the following tools and libraries to implement our asset pipeline: + +1. *Model Loading*: We'll use the tinygltf library to parse glTF files. This library provides: + - Comprehensive support for the glTF 2.0 specification + - Efficient parsing of binary glTF files + - Access to all glTF components (meshes, materials, animations, etc.) + +2. *Texture Loading*: We'll use the KTX-Software library to load KTX2 textures, which offers: + - Support for loading and transcoding Basis Universal compressed textures + - Efficient mipmap handling + - Integration with Vulkan texture formats + +3. *Asset Conversion*: For converting development assets to production assets, we'll use: + - KTX-Tools for texture conversion (PNG/JPEG to KTX2) + - glTF-Transform for model processing and optimization + - Custom scripts for automating the conversion process + +=== Integration with Engine Architecture + +Our model loading system will integrate with the engine architecture from previous chapters: + +1. *Resource Management*: We'll leverage the resource management system from the Engine Architecture chapter to: + - Cache loaded models and textures + - Implement reference counting for efficient memory management + - Support asynchronous loading of models + +2. *Component System*: We'll create the following components: + - ModelComponent: Manages model rendering and animation + - MaterialComponent: Handles material properties and textures + - These components will work with the TransformComponent from the Camera Transformations chapter + +3. *Rendering Pipeline*: Our model loading system will integrate with the rendering pipeline by: + - Providing mesh data for the geometry pass + - Supporting PBR materials for the lighting pass + - Enabling instanced rendering for repeated models + +=== Artist Workflow + +Our workflow for artists will be: + +1. *Development Phase*: + - Artists create models in tools like Blender or Maya + - Export to standard glTF (.gltf) with separate PNG/JPEG textures + - Test with glTF viewers to ensure correct appearance + +2. *Technical Requirements*: + - Right-handed coordinate system with Y-up + - 1 unit = 1 meter scale + - PBR materials using the metallic-roughness workflow + - Textures with power-of-two dimensions + +3. *Conversion Process*: + - Validate models against technical requirements + - Convert textures to KTX2 with Basis Universal compression + - Embed textures into glb files + - Optimize models (remove unused vertices, compress meshes, etc.) + +4. *Integration*: + - Place converted assets in the appropriate directories + - Register assets in the resource management system + - Create entities with appropriate components + +=== Runtime Loading + +At runtime, our engine will: + +1. *Load Models*: + - Parse glb files using tinygltf + - Extract mesh data, materials, and animations + - Create Vulkan buffers for vertices and indices + +2. *Process Materials*: + - Load embedded KTX2 textures + - Create Vulkan image views and samplers + - Set up descriptor sets for PBR rendering + +3. *Handle Animations*: + - Parse animation data from glTF + - Implement skeletal animation system + - Support animation blending and transitions + +4. *Render Models*: + - Use the scene graph to organize models hierarchically + - Apply transformations from the transform component + - Render with appropriate materials and shaders + +By implementing these specific approaches, our engine will have a robust and efficient asset pipeline that aligns with the general concepts discussed earlier in this chapter. + +link:03_model_system.adoc[Next: Implementing the Model Loading System] diff --git a/en/Building_a_Simple_Engine/Loading_Models/03_model_system.adoc b/en/Building_a_Simple_Engine/Loading_Models/03_model_system.adoc new file mode 100644 index 00000000..f24750cb --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/03_model_system.adoc @@ -0,0 +1,396 @@ +::pp: {plus}{plus} + += Loading Models: Implementing the Model Loading System +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Implementing the Model Loading System + +=== Building on glTF Knowledge + +As we learned in the link:../../15_GLTF_KTX2_Migration.html[glTF and KTX2 Migration chapter], glTF is a modern 3D format that supports a wide range of features including PBR materials, animations, and scene hierarchies. In this chapter, we'll leverage these capabilities to build a more robust engine. + +While the previous chapter covered the basics of loading glTF models, here we'll focus on organizing the loaded data into a proper scene graph and implementing animation support. This approach will allow us to create more complex and dynamic scenes. + +In this chapter, we'll not only implement the technical aspects of model loading but also discuss the architectural decisions behind our design and how developers can effectively use this system in their applications. Understanding these concepts is crucial for building a maintainable and extensible engine. + +=== Setting Up Our Engine's Model System + +We'll start with the same tinygltf library setup as in the previous chapter: + +[source,cpp] +---- +// Include tinygltf for model loading +#include +---- + +However, instead of just loading the model data directly into vertex and index buffers, we'll create a more structured approach with proper data classes to represent our scene. + +=== Defining Data Structures + +To handle the rich data provided by glTF, we need to define several data structures: + +[source,cpp] +---- +// Vertex structure with position, normal, color, and texture coordinates +struct Vertex { + glm::vec3 pos; + glm::vec3 normal; + glm::vec3 color; + glm::vec2 texCoord; + + // Binding and attribute descriptions for Vulkan + static vk::VertexInputBindingDescription getBindingDescription() { + return { 0, sizeof(Vertex), vk::VertexInputRate::eVertex }; + } + + static std::array getAttributeDescriptions() { + return { + vk::VertexInputAttributeDescription( 0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, pos) ), + vk::VertexInputAttributeDescription( 1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, normal) ), + vk::VertexInputAttributeDescription( 2, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color) ), + vk::VertexInputAttributeDescription( 3, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, texCoord) ) + }; + } + + // Equality operator and hash function for vertex deduplication + bool operator==(const Vertex& other) const { + return pos == other.pos && normal == other.normal && color == other.color && texCoord == other.texCoord; + } +}; + +// Structure for PBR material properties +struct Material { + glm::vec4 baseColorFactor = glm::vec4(1.0f); + float metallicFactor = 1.0f; + float roughnessFactor = 1.0f; + glm::vec3 emissiveFactor = glm::vec3(0.0f); + + int baseColorTextureIndex = -1; + int metallicRoughnessTextureIndex = -1; + int normalTextureIndex = -1; + int occlusionTextureIndex = -1; + int emissiveTextureIndex = -1; +}; + +// Structure for a mesh with vertices, indices, and material +struct Mesh { + std::vector vertices; + std::vector indices; + int materialIndex = -1; +}; +---- + +=== Why We Need a Scene Graph + +A scene graph is a tree-like data structure that organizes the spatial representation of a graphical scene. While it might seem tempting to use a simple collection or map to store 3D objects, scene graphs offer several critical advantages: + +==== Benefits of Using a Scene Graph + +* *Hierarchical Transformations*: Scene graphs allow child objects to inherit transformations from their parents. When you move, rotate, or scale a parent node, all its children are automatically transformed relative to the parent. This is essential for complex models like characters where moving the torso should also move the attached limbs. + +* *Spatial Organization*: Scene graphs organize objects based on their spatial relationships, making it easier to perform operations like culling, collision detection, and level-of-detail management. + +* *Animation Support*: Hierarchical structures are crucial for skeletal animations, where movements propagate through a chain of bones. + +* *Scene Management*: Scene graphs facilitate operations like saving/loading scenes, instancing (reusing the same model in different locations), and dynamic scene modifications. + +==== Scene Graphs vs. Simple Collections + +Unlike a simple map or array of objects, a scene graph: + +* Maintains parent-child relationships between objects +* Automatically propagates transformations down the hierarchy +* Provides a natural structure for traversal algorithms (rendering, picking, collision) +* Supports local-to-global coordinate transformations + +For example, with a flat collection of objects, if you wanted to move a character and all its equipment, you'd need to update each piece individually. With a scene graph, you simply move the character node, and all attached equipment moves automatically. + +==== Scene Graphs vs. Spatial Partitioning Systems (Game Maps) + +It's important to distinguish between scene graphs and spatial partitioning systems (often referred to as "game maps" in engine development): + +* *Scene Graphs* focus on hierarchical relationships and transformations between objects. +* *Spatial Partitioning Systems* focus on efficiently organizing objects in space for collision detection, visibility determination, and physics calculations. + +While scene graphs organize objects based on logical relationships (like a character and its equipment), spatial partitioning systems organize objects based on their physical location in the game world. + +===== Common Spatial Partitioning Systems + +Several spatial partitioning techniques are used in game development: + +* *Octrees*: Divide 3D space into eight equal octants recursively. Used for large open worlds where objects are distributed unevenly. Octrees adapt to object density, with more subdivisions in crowded areas. + +* *Binary Space Partitioning (BSP)*: Recursively divides space using planes. Particularly efficient for indoor environments and was popularized by early first-person shooters like Doom and Quake. + +* *Quadtrees*: The 2D equivalent of octrees, dividing space into four quadrants recursively. Commonly used for 2D games or for terrain in 3D games. + +* *Axis-Aligned Bounding Boxes (AABB) Trees*: Organize objects based on their bounding boxes, creating a hierarchy that allows for efficient collision checks. + +* *Portal Systems*: Divide the world into "rooms" connected by "portals." This approach is particularly effective for indoor environments with distinct areas. + +* *Spatial Hashing*: Maps 3D positions to a hash table, allowing for constant-time lookups of nearby objects. Useful for particle systems and other scenarios with many similar-sized objects. + +* *Bounding Volume Hierarchies (BVH)*: Create a tree of nested bounding volumes, allowing for efficient ray casting and collision detection. + +===== Spatial Partitioning in Popular Engines + +Different game engines use different spatial partitioning systems, often combining multiple approaches: + +* *Unreal Engine*: Uses a combination of octrees for the overall world and BSP for detailed indoor environments. Also uses a custom system called "Unreal Visibility Determination" that combines portals and potentially visible sets. + +* *Unity*: Implements a quadtree/octree hybrid system for its physics and rendering. For navigation, it uses a navigation mesh system. + +* *CryEngine/CRYENGINE*: Uses octrees for outdoor environments and portal systems for indoor areas. + +* *Godot*: Employs BVH trees for its physics engine and octrees for rendering. + +* *Source Engine (Valve)*: Famous for its Binary Space Partitioning (BSP) combined with a portal system called "Potentially Visible Set" (PVS). + +* *id Tech (id Software)*: Early versions (Doom, Quake) pioneered BSP usage. Later versions use combinations of BSP, octrees, and portal systems. + +* *Frostbite (EA)*: Uses a hierarchical grid system combined with octrees for its large-scale destructible environments. + +In practice, many modern engines use hybrid approaches, selecting the appropriate partitioning system based on the specific needs of different parts of the game world. + +=== Architectural Decisions + +When designing our model system, we made several key architectural decisions: + +* *Node-Based Structure*: We use a node-based approach where each node can have a mesh, transformation, and children. This provides flexibility for complex scene hierarchies. + +* *Separation of Concerns*: We separate geometric data (vertices, indices) from material properties and transformations, allowing for more efficient memory use and easier updates. + +* *Animation-Ready*: Our design includes dedicated structures for animations, supporting keyframe interpolation and different animation channels (translation, rotation, scale). + +* *Memory Management*: We use a centralized ownership model where the Model class owns all nodes, simplifying cleanup and preventing memory leaks. + +* *Efficient Traversal*: We maintain both a hierarchical structure (`nodes`) and a flat list (`linearNodes`) to support different traversal patterns efficiently. + +=== How Developers Would Use the Model System + +Here's how a developer would typically use this model system in their application: + +==== Loading and Initializing Models + +[source,cpp] +---- +// Create and load a model +Model* characterModel = new Model(); +loadFromFile(characterModel, "character.gltf"); + +// Find specific nodes in the model +Node* headNode = characterModel->findNode("Head"); +Node* weaponAttachPoint = characterModel->findNode("RightHand"); + +// Attach additional objects to the model +Model* weaponModel = new Model(); +loadFromFile(weaponModel, "weapon.gltf"); +weaponAttachPoint->children.push_back(weaponModel->nodes[0]); +---- + +==== Updating and Animating Models + +[source,cpp] +---- +// Play an animation +float deltaTime = 0.016f; // 16ms or ~60 FPS NB: Keep this relative to frame +instead of a constant in actual code as some systems are faster resulting in +faster animation on a constant that isn't tied to the frame time. +characterModel->updateAnimation(0, deltaTime); // Play the first animation + +// Manually transform nodes +headNode->rotation = glm::rotate(headNode->rotation, glm::radians(15.0f), glm::vec3(0, 1, 0)); // Look to the side +---- + +==== Rendering Models + +[source,cpp] +---- +void renderModel(Model* model, VkCommandBuffer commandBuffer) { + // Traverse all nodes in the model + for (auto& node : model->linearNodes) { + if (node->mesh.indices.size() > 0) { + // Get the global transformation matrix + glm::mat4 nodeMatrix = node->getGlobalMatrix(); + + // Update uniform buffer with the node's transformation + updateUniformBuffer(nodeMatrix); + + // Bind the appropriate material + if (node->mesh.materialIndex >= 0) { + bindMaterial(model->materials[node->mesh.materialIndex]); + } + + // Draw the mesh + vkCmdDrawIndexed(commandBuffer, + static_cast(node->mesh.indices.size()), + 1, 0, 0, 0); + } + } +} +---- + +=== Back to our tutorial + +Now that you've seen how the model system API is used from a hypothetical +developer's perspective, it's time to implement this functionality. +In the following sections, we'll guide you through implementing the scene +graph, animation system, and model class that will power the engine. + +=== Implementing a Scene Graph + +Now let's look at the implementation of our scene graph structure: + +[source,cpp] +---- +// Structure for a node in the scene graph +struct Node { + Node* parent = nullptr; + std::vector children; + Mesh mesh; + glm::mat4 matrix = glm::mat4(1.0f); + + // For animation + glm::vec3 translation = glm::vec3(0.0f); + glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 scale = glm::vec3(1.0f); + + glm::mat4 getLocalMatrix() { + return glm::translate(glm::mat4(1.0f), translation) * + glm::toMat4(rotation) * + glm::scale(glm::mat4(1.0f), scale) * + matrix; + } + + glm::mat4 getGlobalMatrix() { + glm::mat4 m = getLocalMatrix(); + Node* p = parent; + while (p) { + m = p->getLocalMatrix() * m; + p = p->parent; + } + return m; + } +}; +---- + +=== Animation Structures + +To support animations, we need additional structures: + +[source,cpp] +---- +// Structure for animation keyframes +struct AnimationChannel { + enum PathType { TRANSLATION, ROTATION, SCALE }; + PathType path; + Node* node = nullptr; + uint32_t samplerIndex; +}; + +// Structure for animation interpolation +struct AnimationSampler { + enum InterpolationType { LINEAR, STEP, CUBICSPLINE }; + InterpolationType interpolation; + std::vector inputs; // Key frame timestamps + std::vector outputsVec4; // Key frame values (for rotations) + std::vector outputsVec3; // Key frame values (for translations and scales) +}; + +// Structure for animation +struct Animation { + std::string name; + std::vector samplers; + std::vector channels; + float start = std::numeric_limits::max(); + float end = std::numeric_limits::min(); + float currentTime = 0.0f; +}; +---- + +=== The Model Class + +Now we can define a Model class that brings everything together: + +[source,cpp] +---- +// Structure for a model with nodes, meshes, materials, textures, and animations +struct Model { + std::vector nodes; + std::vector linearNodes; + std::vector materials; + std::vector animations; + + ~Model() { + for (auto node : linearNodes) { + delete node; + } + } + + Node* findNode(const std::string& name) { + for (auto node : linearNodes) { + if (node->name == name) { + return node; + } + } + return nullptr; + } + + void updateAnimation(uint32_t index, float deltaTime) { + if (animations.empty() || index >= animations.size()) { + return; + } + + Animation& animation = animations[index]; + animation.currentTime += deltaTime; + if (animation.currentTime > animation.end) { + animation.currentTime = animation.start; + } + + for (auto& channel : animation.channels) { + AnimationSampler& sampler = animation.samplers[channel.samplerIndex]; + + // Find the current key frame + for (size_t i = 0; i < sampler.inputs.size() - 1; i++) { + if (animation.currentTime >= sampler.inputs[i] && animation.currentTime <= sampler.inputs[i + 1]) { + float t = (animation.currentTime - sampler.inputs[i]) / (sampler.inputs[i + 1] - sampler.inputs[i]); + + switch (channel.path) { + case AnimationChannel::TRANSLATION: { + glm::vec3 start = sampler.outputsVec3[i]; + glm::vec3 end = sampler.outputsVec3[i + 1]; + channel.node->translation = glm::mix(start, end, t); + break; + } + case AnimationChannel::ROTATION: { + glm::quat start = glm::quat(sampler.outputsVec4[i].w, sampler.outputsVec4[i].x, sampler.outputsVec4[i].y, sampler.outputsVec4[i].z); + glm::quat end = glm::quat(sampler.outputsVec4[i + 1].w, sampler.outputsVec4[i + 1].x, sampler.outputsVec4[i + 1].y, sampler.outputsVec4[i + 1].z); + channel.node->rotation = glm::slerp(start, end, t); + break; + } + case AnimationChannel::SCALE: { + glm::vec3 start = sampler.outputsVec3[i]; + glm::vec3 end = sampler.outputsVec3[i + 1]; + channel.node->scale = glm::mix(start, end, t); + break; + } + } + break; + } + } + } + } +}; +---- + +=== Next Steps: Loading glTF Files + +Now that we've designed our model system's architecture and implemented the core data structures, the next step is to actually load 3D models from glTF files. In the next chapter, we'll explore how to parse glTF files using the tinygltf library and populate our scene graph with the loaded data. We'll learn how to extract meshes, materials, textures, and animations from glTF files and convert them into our engine's internal representation. + +link:02_project_setup.adoc[Previous: Setting Up the Project] | link:04_loading_gltf.adoc[Next: Loading a glTF Model] diff --git a/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc b/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc new file mode 100644 index 00000000..aa07e210 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/04_loading_gltf.adoc @@ -0,0 +1,808 @@ +:pp: {plus}{plus} + += Loading Models: Understanding glTF + +== Understanding glTF + +=== What is glTF? + +glTF (GL Transmission Format) is a standard 3D file format developed by the Khronos Group (the same organization behind OpenGL and Vulkan). It's often called the "JPEG of 3D" because it aims to be a universal, efficient format for 3D content. + +The main purpose of glTF is to bridge the gap between 3D content creation tools (like Blender, Maya, 3ds Max) and real-time rendering applications like games and visualization tools. Before glTF, developers often had to create custom exporters or use intermediate formats that weren't optimized for real-time rendering. + +Key advantages of glTF include: + +* *Efficiency*: Optimized for loading speed and rendering performance with minimal processing +* *Completeness*: Contains geometry, materials, textures, animations, and scene hierarchy in a single format +* *PBR Support*: Built-in support for modern physically-based rendering materials +* *Standardization*: Widely adopted across the industry, reducing the need for custom exporters +* *Extensibility*: Supports extensions for vendor-specific features while maintaining compatibility + +=== glTF File Structure and Data Organization + +A glTF file contains several key components organized in a structured way: + +* *Scenes and Nodes*: The hierarchical structure that organizes objects in a scene graph +* *Meshes*: The 3D geometry data (vertices, indices, attributes like normals and UVs) +* *Materials*: Surface properties using a physically-based rendering (PBR) model +* *Textures and Images*: Visual data for materials, with support for various texture types +* *Animations*: Keyframe data for animating nodes (position, rotation, scale) +* *Skins*: Data for skeletal animations (joint hierarchies and vertex weights) +* *Cameras*: Perspective or orthographic camera definitions + +==== The Buffer System: Efficient Binary Data Storage + +One of glTF's most powerful features is its three-level buffer system: + +1. *Buffers*: Raw binary data blocks (like files on disk) +2. *BufferViews*: Views into buffers with specific offset and length +3. *Accessors*: Descriptions of how to interpret data in a bufferView (type, component type, count, etc.) + +This system allows different attributes (positions, normals, UVs) to share the same underlying buffer, reducing memory usage and file size. For example: + +* A single buffer might contain all vertex data +* One bufferView points to the position data within that buffer +* Another bufferView points to the normal data +* Accessors describe how to interpret each bufferView (e.g., as vec3 floats) + +=== Using the tinygltf Library for Efficient Parsing + +Rather than writing a glTF parser from scratch (which would be a significant undertaking), we'll use the tinygltf library: + +* It's a lightweight, header-only C++ library that's easy to integrate +* It handles both .gltf and .glb formats transparently +* It manages the complex task of parsing JSON and binary data +* It provides a clean API for accessing all glTF components +* It handles the details of the buffer system, including base64-encoded data + +Using tinygltf allows us to focus on the higher-level task of converting the parsed data into our engine's structures rather than dealing with the low-level details of parsing JSON and binary data. + +=== Implementing a Robust glTF Loader + +When implementing a production-ready glTF loader, several considerations come into play: + +* *Error Handling*: Robust handling of malformed files and graceful failure +* *Format Detection*: Supporting both .gltf and .glb formats +* *Memory Management*: Efficient allocation and handling of large data +* *Extension Support*: Handling optional glTF extensions + +Let's look at how we implement the initial file loading: + +[source,cpp] +---- +void loadModel(const std::string& modelPath) { + // Create a tinygltf loader + tinygltf::Model gltfModel; + tinygltf::TinyGLTF loader; + std::string err, warn; + + // Detect file extension to determine which loader to use + bool ret = false; + std::string extension = modelPath.substr(modelPath.find_last_of(".") + 1); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + + if (extension == "glb") { + ret = loader.LoadBinaryFromFile(&gltfModel, &err, &warn, modelPath); + } else if (extension == "gltf") { + ret = loader.LoadASCIIFromFile(&gltfModel, &err, &warn, modelPath); + } else { + err = "Unsupported file extension: " + extension + ". Expected .gltf or .glb"; + } + + // Handle errors and warnings + if (!warn.empty()) { + std::cout << "glTF warning: " << warn << std::endl; + } + if (!err.empty()) { + std::cout << "glTF error: " << err << std::endl; + } + if (!ret) { + throw std::runtime_error("Failed to load glTF model"); + } + + // Clear existing model data + model = Model(); + + // Process the loaded data (covered in the following sections) +} +---- + +Supporting both .gltf and .glb formats gives artists flexibility in their workflow. + +glTF comes in two formats, each with its own advantages: + +* *.gltf*: A JSON-based format with external binary and image files +- Human-readable and easier to debug +- Allows for easier asset management (textures as separate files) +- Better for development workflows +* *.glb*: A binary format that combines everything in a single file +- More compact and efficient for distribution +- Reduces the number of file operations during loading +- Better for deployment and distribution + +=== Understanding Physically Based Rendering (PBR) Materials + +[NOTE] +==== +This section provides a brief overview of PBR materials as they relate to glTF loading. For a more comprehensive explanation of PBR concepts and lighting models, please refer to the link:../../Lighting_Materials/02_lighting_models.adoc#physically-based-rendering-pbr[Physically Based Rendering section] in the Lighting Materials chapter. +==== + +Materials define how surfaces look when rendered. Modern games and engines use Physically Based Rendering (PBR), which simulates how light interacts with real-world materials based on physical principles. + +==== The Evolution of Material Systems + +Material systems in 3D graphics have evolved significantly: + +1. *Basic Materials (1990s)*: Simple diffuse colors with optional specular highlights +2. *Multi-Texture Materials (2000s)*: Multiple texture maps combined for different effects +3. *Shader-Based Materials (Late 2000s)*: Custom shader programs for advanced effects +4. *Physically Based Rendering (2010s)*: Materials based on physical properties of real-world surfaces + +PBR represents the current state of the art in real-time graphics. It provides more realistic results across different lighting conditions and ensures consistent appearance regardless of the environment. + +==== Key PBR Material Properties + +The PBR model in glTF is based on the "metallic-roughness" workflow, which uses these key properties: + +* *Base Color*: The albedo or diffuse color of the surface (RGB or texture) +* *Metalness*: How metal-like the surface is (0.0 = non-metal, 1.0 = metal) + - Metals have no diffuse reflection but high specular reflection + - Non-metals (dielectrics) have diffuse reflection and minimal specular reflection +* *Roughness*: How smooth or rough the surface is (0.0 = mirror-like, 1.0 = rough) + - Controls the microsurface detail that causes light scattering + - Affects the sharpness of reflections and specular highlights +* *Normal Map*: Adds surface detail without extra geometry + - Perturbs surface normals to create the illusion of additional detail + - More efficient than adding actual geometry +* *Occlusion Map*: Approximates self-shadowing within surface crevices + - Darkens areas that would receive less ambient light + - Enhances the perception of depth and detail +* *Emissive*: Makes the surface emit light (RGB or texture) + - Used for glowing objects like screens, lights, or neon signs + - Not affected by scene lighting + +These properties can be specified as constant values or as texture maps for +spatial variation across the surface. We'll go into details about PBR in the +next few chapters. + +==== Texture Formats and Compression + +In our engine, we use KTX2 with Basis Universal compression for textures. This approach offers several advantages: + +* *Reduced File Size*: Basis Universal compression significantly reduces texture sizes while maintaining visual quality +* *GPU-Ready Formats*: KTX2 textures can be directly transcoded to platform-specific GPU formats +* *Cross-Platform Compatibility*: Basis Universal textures work across different platforms and graphics APIs +* *Mipmap Support*: KTX2 includes support for mipmaps, improving rendering quality and performance + +===== Embedded Textures in glTF/glb + +The glTF format supports two ways to include textures: + +1. *External References*: The .gltf file references external image files +2. *Embedded Data*: Images are embedded directly in the .glb file as binary data + +For our engine, we use the .glb format with embedded KTX2 textures. This approach: + +* Reduces the number of file operations during loading +* Ensures all textures are always available with the model +* Simplifies asset management and distribution + +The glTF specification supports embedded textures through the `bufferView` property of image objects. When using KTX2 textures, the `mimeType` is set to `"image/ktx2"` to indicate the format. + +The texture loading process involves several complex steps that bridge the gap between glTF's abstract texture references and Vulkan's low-level GPU resources. + +=== Texture Loading: glTF Texture Iteration and Metadata Extraction + +First, we iterate through the glTF model's texture definitions and extracting the fundamental information needed to locate and identify each texture resource. + +[source,cpp] +---- +// First, load all textures from the model +std::vector textures; +for (size_t i = 0; i < gltfModel.textures.size(); i++) { + const auto& texture = gltfModel.textures[i]; + const auto& image = gltfModel.images[texture.source]; + + Texture tex; + tex.name = image.name.empty() ? "texture_" + std::to_string(i) : image.name; +---- + +The glTF texture system uses an indirection approach where textures reference images, and images contain the actual pixel data or references to it. This separation allows multiple textures to share the same image data but with different sampling parameters (like different filtering or wrapping modes). Our iteration process builds a comprehensive inventory of all texture resources that materials will eventually reference. + +The naming strategy provides essential debugging and asset management capabilities. When artists create textures in their 3D applications, meaningful names help developers identify which textures serve which purposes during development. The fallback naming scheme ensures every texture has a unique identifier even when artists haven't provided descriptive names. + +=== Texture Loading: Format Detection and Buffer Access + +Next, we need to figure out whether textures are embedded in the glTF file and identify their format, setting up the foundation for appropriate loading strategies. + +[source,cpp] +---- + // Check if the image is embedded as KTX2 + if (image.mimeType == "image/ktx2" && image.bufferView >= 0) { + // Get the buffer view that contains the KTX2 data + const auto& bufferView = gltfModel.bufferViews[image.bufferView]; + const auto& buffer = gltfModel.buffers[bufferView.buffer]; + + // Extract the KTX2 data from the buffer + const uint8_t* ktx2Data = buffer.data.data() + bufferView.byteOffset; + size_t ktx2Size = bufferView.byteLength; +---- + +The MIME type detection ensures we're working with KTX2 format specifically, which provides several advantages over traditional image formats like PNG or JPEG. KTX2 is designed specifically for GPU textures and supports advanced features like basis universal compression, multiple mipmap levels, and direct GPU format compatibility. The bufferView check confirms that the image data is embedded within the glTF file rather than referenced externally. + +The buffer access pattern demonstrates glTF's sophisticated data organization system. Rather than copying data unnecessarily, we obtain direct pointers to the KTX2 data within the loaded glTF buffer. This approach minimizes memory usage and avoids expensive copy operations, which is particularly important when dealing with large texture datasets that can easily consume hundreds of megabytes. + +=== Texture Loading: KTX2 Parsing and Validation + +Now we need to load the KTX2 texture data using the specialized KTX-Software library and perform initial validation to ensure the texture data is usable. + +[source,cpp] +---- + // Load the KTX2 texture using KTX-Software library + ktxTexture2* ktxTexture = nullptr; + KTX_error_code result = ktxTexture2_CreateFromMemory( + ktx2Data, ktx2Size, + KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, + &ktxTexture + ); + + if (result != KTX_SUCCESS) { + std::cerr << "Failed to load KTX2 texture: " << ktxErrorString(result) << std::endl; + continue; + } +---- + +The KTX-Software library provides robust parsing of the complex KTX2 format, handling details like multiple mipmap levels, various pixel formats, and metadata that would be extremely complex to implement correctly from scratch. The `KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT` flag instructs the library to immediately load the actual pixel data into memory, preparing it for subsequent processing steps. + +Error handling at this stage is crucial because texture files can become corrupted during asset pipeline processing or file transfer. By continuing with the next texture when one fails to load, we ensure that a single problematic texture doesn't prevent the entire model from loading. This graceful degradation approach is essential for robust production systems where content issues shouldn't crash the application. + +=== Texture Loading: Basis Universal Transcoding + +Next, we handle the transcoding process that converts Basis Universal compressed textures into GPU-native formats for optimal runtime performance. + +[source,cpp] +---- + // If the texture uses Basis Universal compression, transcode it to a GPU-friendly format + if (ktxTexture->isCompressed && ktxTexture2_NeedsTranscoding(ktxTexture)) { + // Choose the appropriate format based on GPU capabilities + ktx_transcode_fmt_e transcodeFmt = KTX_TTF_BC7_RGBA; + + // For devices that don't support BC7, use alternatives + // if (!deviceSupportsBC7) { + // transcodeFmt = KTX_TTF_ASTC_4x4_RGBA; + // } + // if (!deviceSupportsASTC) { + // transcodeFmt = KTX_TTF_ETC2_RGBA; + // } + + // Transcode the texture + result = ktxTexture2_TranscodeBasis(ktxTexture, transcodeFmt, 0); + if (result != KTX_SUCCESS) { + std::cerr << "Failed to transcode KTX2 texture: " << ktxErrorString(result) << std::endl; + ktxTexture2_Destroy(ktxTexture); + continue; + } + } +---- + +Basis Universal represents a revolutionary approach to texture compression that solves a fundamental problem in cross-platform development: different GPUs support different texture compression formats. Traditional approaches required storing multiple texture versions for different platforms, dramatically increasing storage requirements. Basis Universal stores textures in an intermediate format that can be quickly transcoded to any GPU-native format at load time. + +The format selection logic (shown in commented form) demonstrates how production systems handle GPU capability differences. Desktop GPUs typically support BC7 compression which provides excellent quality, while mobile GPUs often use ASTC or ETC2 formats. The transcoding process happens at runtime based on the actual capabilities of the target GPU, ensuring optimal performance and quality on every platform. + +The transcoding operation itself is computationally intensive but happens only once during asset loading. The resulting GPU-native format provides significantly better performance during rendering compared to uncompressed textures, making the upfront transcoding cost worthwhile. Failed transcoding attempts trigger cleanup of partially processed resources, preventing memory leaks in error conditions. + +=== Texture Loading: Vulkan Resource Creation and GPU Upload + +Finally, create the Vulkan resources needed for GPU rendering and uploads the processed texture data to video memory. + +[source,cpp] +---- + // Create Vulkan image, memory, and view + vk::Format format = static_cast(ktxTexture2_GetVkFormat(ktxTexture)); + vk::Extent3D extent{ + static_cast(ktxTexture->baseWidth), + static_cast(ktxTexture->baseHeight), + static_cast(ktxTexture->baseDepth) + }; + uint32_t mipLevels = ktxTexture->numLevels; + + // Create the Vulkan image + vk::ImageCreateInfo imageCreateInfo{ + .imageType = vk::ImageType::e2D, + .format = format, + .extent = extent, + .mipLevels = mipLevels, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = vk::ImageTiling::eOptimal, + .usage = vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined + }; + + // Create the image, allocate memory, and bind them + // ... (code omitted for brevity) + + // Upload the texture data to the image + ktxTexture2_VkUploadEx(ktxTexture, &ktxVulkanTexture, &vkDevice, &vkQueue, + &ktxVulkanDeviceMemory, &ktxVulkanImage, + &ktxVulkanImageView, &ktxVulkanImageLayout, + &ktxVulkanImageMemory); + + // Store the Vulkan resources in our texture object + tex.image = ktxVulkanImage; + tex.imageView = ktxVulkanImageView; + tex.memory = ktxVulkanImageMemory; + + // Clean up KTX resources + ktxTexture2_Destroy(ktxTexture); + } else { + // Handle other image formats or external references + // ... (code omitted for brevity) + } + + // Create a sampler for the texture + VkSamplerCreateInfo samplerInfo = {}; + // ... (code omitted for brevity) + + textures.push_back(tex); +} + +// Now load materials and associate them with textures +for (const auto& material : gltfModel.materials) { + Material mat; + + // Base color + if (material.pbrMetallicRoughness.baseColorFactor.size() == 4) { + mat.baseColorFactor.r = material.pbrMetallicRoughness.baseColorFactor[0]; + mat.baseColorFactor.g = material.pbrMetallicRoughness.baseColorFactor[1]; + mat.baseColorFactor.b = material.pbrMetallicRoughness.baseColorFactor[2]; + mat.baseColorFactor.a = material.pbrMetallicRoughness.baseColorFactor[3]; + } + + // Metallic and roughness factors + mat.metallicFactor = material.pbrMetallicRoughness.metallicFactor; + mat.roughnessFactor = material.pbrMetallicRoughness.roughnessFactor; + + // Associate textures with the material + if (material.pbrMetallicRoughness.baseColorTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.pbrMetallicRoughness.baseColorTexture.index]; + mat.baseColorTexture = &textures[texture.source]; + } + + if (material.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.pbrMetallicRoughness.metallicRoughnessTexture.index]; + mat.metallicRoughnessTexture = &textures[texture.source]; + } + + if (material.normalTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.normalTexture.index]; + mat.normalTexture = &textures[texture.source]; + } + + if (material.occlusionTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.occlusionTexture.index]; + mat.occlusionTexture = &textures[texture.source]; + } + + if (material.emissiveTexture.index >= 0) { + const auto& texture = gltfModel.textures[material.emissiveTexture.index]; + mat.emissiveTexture = &textures[texture.source]; + } + + model.materials.push_back(mat); +} +---- + +Now, let's talk about how this all fits together. + +=== Understanding Scene Graphs and Hierarchical Transformations + +A scene graph is a hierarchical tree-like data structure that organizes the spatial representation of a 3D scene. It's a fundamental concept in computer graphics and game engines, serving as the backbone for organizing complex scenes. + +==== Why Scene Graphs Matter + +Scene graphs offer several critical advantages over flat collections of objects: + +* *Hierarchical Transformations*: Children inherit transformations from their parents, making it natural to model complex relationships +* *Spatial Organization*: Objects are organized based on their logical relationships, making scene management easier +* *Animation Support*: Hierarchical structures are crucial for skeletal animations and complex movement patterns +* *Efficient Traversal*: Enables optimized rendering, culling, and picking operations +* *Instancing Support*: The same object can appear multiple times with different transformations + +Consider these practical examples: + +1. *Character with Equipment*: When a character moves, all attached equipment (weapons, armor) should move with it. With a scene graph, you move the character node, and all child nodes automatically inherit the transformation. + +2. *Vehicle with Moving Parts*: A vehicle might have wheels that rotate independently while the whole vehicle moves. A scene graph makes this hierarchy of movements natural to express. + +3. *Articulated Animations*: Characters with skeletons need joints that move relative to their parent joints. A scene graph directly models this parent-child relationship. + +==== Transformations in Scene Graphs + +One of the most powerful aspects of scene graphs is how they handle transformations: + +* Each node has a *local transformation* relative to its parent +* The *global transformation* is calculated by combining the node's local transformation with its parent's global transformation +* This allows for intuitive modeling of complex hierarchical movements + +The transformation pipeline typically works like this: + +1. Each node stores its local transformation (translation, rotation, scale) +2. When rendering, we calculate the global transformation by multiplying with parent transformations +3. This global transformation is used to position the object in world space + +Here's how we build a scene graph from glTF data: + +[source,cpp] +---- +// First pass: create all nodes +for (size_t i = 0; i < gltfModel.nodes.size(); i++) { + const auto& node = gltfModel.nodes[i]; + model.linearNodes[i] = new Node(); + model.linearNodes[i]->index = static_cast(i); + model.linearNodes[i]->name = node.name; + + // Get transformation data + if (node.translation.size() == 3) { + model.linearNodes[i]->translation = glm::vec3( + node.translation[0], node.translation[1], node.translation[2] + ); + } + // ... handle rotation and scale +} + +// Second pass: establish parent-child relationships +for (size_t i = 0; i < gltfModel.nodes.size(); i++) { + const auto& node = gltfModel.nodes[i]; + for (int childIdx : node.children) { + model.linearNodes[childIdx]->parent = model.linearNodes[i]; + model.linearNodes[i]->children.push_back(model.linearNodes[childIdx]); + } +} +---- + +We use a two-pass approach to ensure all nodes exist before we try to link them together. + +=== Understanding 3D Geometry and Mesh Data + +3D models are represented as meshes - collections of vertices, edges, and faces that define the shape of an object. Understanding how this data is structured is crucial for efficient rendering. + +==== The Building Blocks of 3D Models + +The fundamental components of 3D geometry are: + +* *Vertices*: Points in 3D space that define the shape +* *Indices*: References to vertices that define how they connect to form triangles +* *Attributes*: Additional data associated with vertices: + - *Positions*: 3D coordinates (x, y, z) + - *Normals*: Direction vectors perpendicular to the surface (for lighting calculations) + - *Texture Coordinates (UVs)*: 2D coordinates for mapping textures onto the surface + - *Tangents and Bitangents*: Vectors used for normal mapping + - *Colors*: Per-vertex color data + - *Skinning Weights and Indices*: For skeletal animations + +Modern 3D graphics use triangle meshes because: + +* Triangles are always planar (three points define a plane) +* Triangles are the simplest polygon that can represent any surface +* Graphics hardware is optimized for triangle processing + +==== Mesh Organization in glTF + +glTF organizes mesh data in a way that's efficient for both storage and rendering: + +* *Meshes*: Collections of primitives that form a logical object +* *Primitives*: Individual parts of a mesh, each with its own material +* *Attributes*: Vertex data like positions, normals, and texture coordinates +* *Indices*: References to vertices that define triangles + +This organization allows for: + +* Efficient memory use through data sharing +* Material variation within a single mesh +* Optimized rendering through batching + +Here's how we extract mesh data: + +[source,cpp] +---- +// Load meshes +for (size_t i = 0; i < gltfModel.nodes.size(); i++) { + const auto& node = gltfModel.nodes[i]; + if (node.mesh >= 0) { + const auto& mesh = gltfModel.meshes[node.mesh]; + + // Process each primitive + for (const auto& primitive : mesh.primitives) { + Mesh newMesh; + + // Set material + if (primitive.material >= 0) { + newMesh.materialIndex = primitive.material; + } + + // Extract vertex positions, normals, and texture coordinates + // ... (code omitted for brevity) + + // Extract indices that define triangles + // ... (code omitted for brevity) + + // Assign the mesh to the node + model.linearNodes[i]->mesh = newMesh; + } + } +} +---- + +=== Understanding Animation Systems + +Animation is what transforms static 3D models into living, breathing entities in our virtual worlds. A robust animation system is essential for creating engaging and dynamic 3D applications. + +==== Animation Techniques in 3D Graphics + +Several animation techniques are commonly used in 3D graphics: + +* *Keyframe Animation*: Defining specific poses at specific times, with interpolation between them +* *Skeletal Animation*: Using a hierarchy of bones to deform a mesh +* *Morph Target Animation*: Interpolating between predefined mesh shapes +* *Procedural Animation*: Generating animation through algorithms and physics +* *Particle Systems*: Animating many small elements with simple rules + +Modern games typically use a combination of these techniques, with skeletal animation forming the backbone of character movement. + +==== Core Animation Concepts + +Several key concepts are fundamental to understanding animation systems: + +* *Keyframes*: Specific points in time where animation values are explicitly defined +* *Interpolation*: Calculating values between keyframes to create smooth motion +* *Channels*: Targeting specific properties (like position or rotation) for animation +* *Blending*: Combining multiple animations with different weights +* *Retargeting*: Applying animations created for one model to another + +==== The glTF Animation System + +glTF uses a flexible animation system that can represent various animation techniques: + +* *Animations*: Collections of channels and samplers +* *Channels*: Links between samplers and node properties (translation, rotation, scale) +* *Samplers*: Keyframe data with timestamps, values, and interpolation methods +* *Targets*: The properties being animated (translation, rotation, scale, or weights for morph targets) + +glTF supports three interpolation methods: +* *LINEAR*: Smooth transitions with constant velocity +* *STEP*: Sudden changes with no interpolation +* *CUBICSPLINE*: Smooth curves with control points for acceleration and deceleration + +This system allows for complex animations that can target specific parts of a model independently, enabling actions like walking, facial expressions, and complex interactions. + +Here's how we load animation data: + +[source,cpp] +---- +// Load animations +for (const auto& anim : gltfModel.animations) { + Animation animation; + animation.name = anim.name; + + // Load keyframe data + for (const auto& sampler : anim.samplers) { + AnimationSampler animSampler{}; + + // Set interpolation type (LINEAR, STEP, or CUBICSPLINE) + // ... (code omitted for brevity) + + // Extract keyframe times and values + // ... (code omitted for brevity) + + animation.samplers.push_back(animSampler); + } + + // Connect samplers to node properties + for (const auto& channel : anim.channels) { + AnimationChannel animChannel{}; + + // Set target node and property (translation, rotation, or scale) + // ... (code omitted for brevity) + + animation.channels.push_back(animChannel); + } + + model.animations.push_back(animation); +} +---- + +=== Integration with the Rendering Pipeline + +Now that we've loaded our model data, let's discuss how it integrates with the rest of our rendering pipeline. + +==== From Asset Loading to Rendering + +The journey from a glTF file to pixels on the screen involves several stages: + +1. *Asset Loading*: The glTF loader populates our Model, Node, Mesh, and Material structures +2. *Scene Management*: The engine maintains a collection of loaded models in the scene +3. *Update Loop*: Each frame, animations are updated based on elapsed time +4. *Culling*: The engine determines which objects are potentially visible +5. *Rendering*: The scene graph is traversed, and each visible mesh is rendered with its material + +This pipeline allows for efficient rendering of complex scenes with animated models. + +==== Rendering Optimizations + +Several optimizations can improve the performance of model rendering: + +* *Batching*: Group similar objects to reduce draw calls +* *Instancing*: Render multiple instances of the same mesh with different transforms +* *Level of Detail (LOD)*: Use simpler versions of models at greater distances +* *Frustum Culling*: Skip rendering objects outside the camera's view +* *Occlusion Culling*: Skip rendering objects hidden behind other objects + +==== Memory Management Considerations + +When loading models, especially large ones, memory management becomes crucial: + +* *Vertex Data*: Store in GPU buffers for efficient rendering +* *Indices*: Use 16-bit indices when possible to save memory +* *Textures*: Use KTX2 with Basis Universal compression to significantly reduce memory usage +* *Instancing*: Reuse the same model data for multiple instances with different transforms + +===== Efficient Texture Memory Management with KTX2 and Basis Universal + +Textures often consume the majority of GPU memory in 3D applications. KTX2 with Basis Universal compression provides several memory optimization benefits: + +* *Supercompression*: Basis Universal can reduce texture size by 4-10x compared to uncompressed formats +* *GPU-Native Formats*: Textures are transcoded to formats that GPUs can directly sample from, avoiding runtime decompression +* *Mipmaps*: KTX2 supports mipmaps, which not only improve visual quality but also reduce memory usage for distant objects +* *Format Selection*: The transcoder can choose the optimal format based on the target GPU's capabilities: + - BC7 for desktop GPUs (NVIDIA, AMD, Intel) + - ASTC for mobile GPUs (ARM, Qualcomm) + - ETC2 for older mobile GPUs + +===== Integration with Vulkan Rendering Pipeline + +To efficiently integrate KTX2 textures with Vulkan: + +1. *Descriptor Sets*: Create descriptor sets that bind texture image views and samplers to shader binding points +2. *Pipeline Layout*: Define a pipeline layout that includes these descriptor sets +3. *Shader Access*: In shaders, access textures using the appropriate binding points + +Here's a simplified example of setting up descriptor sets for PBR textures: + +[source,cpp] +---- +// Create descriptor set layout for PBR textures +std::array bindings{ + // Base color texture + vk::DescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }, + // Metallic-roughness texture + vk::DescriptorSetLayoutBinding{ + .binding = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }, + // Normal map + vk::DescriptorSetLayoutBinding{ + .binding = 2, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }, + // Occlusion map + vk::DescriptorSetLayoutBinding{ + .binding = 3, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + }, + // Emissive map + vk::DescriptorSetLayoutBinding{ + .binding = 4, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eFragment + } +}; + +vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() +}; + +vk::raii::DescriptorSetLayout descriptorSetLayout(device, layoutInfo); + +// For each material, create a descriptor set and update it with the material's textures +for (const auto& material : model.materials) { + // Allocate descriptor set from the descriptor pool + vk::DescriptorSetAllocateInfo allocInfo{ + .descriptorPool = descriptorPool, + .descriptorSetCount = 1, + .pSetLayouts = &*descriptorSetLayout + }; + + vk::raii::DescriptorSet descriptorSet = std::move(vk::raii::DescriptorSets(device, allocInfo).front()); + + // Update descriptor set with texture image views and samplers + std::vector descriptorWrites; + + if (material.baseColorTexture) { + vk::DescriptorImageInfo imageInfo{ + .sampler = material.baseColorTexture->sampler, + .imageView = material.baseColorTexture->imageView, + .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal + }; + + vk::WriteDescriptorSet write{ + .dstSet = *descriptorSet, + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = vk::DescriptorType::eCombinedImageSampler, + .pImageInfo = &imageInfo + }; + + descriptorWrites.push_back(write); + } + + // Similar writes for other textures + // ... + + device.updateDescriptorSets(descriptorWrites, {}); + + // Store the descriptor set with the material for later use during rendering + material.descriptorSet = *descriptorSet; +} +---- + +===== Best Practices for Texture Memory Management + +To optimize texture memory usage: + +1. *Texture Atlasing*: Combine multiple small textures into a single larger texture to reduce state changes +2. *Mipmap Management*: Generate and use mipmaps for all textures to improve performance and quality +3. *Texture Streaming*: For very large scenes, implement texture streaming to load higher resolution textures only when needed +4. *Memory Budgeting*: Implement a texture budget system that can reduce texture quality when memory is constrained +5. *Format Selection*: Choose the appropriate format based on the texture content: + - BC7/ASTC for color textures with alpha + - BC1/ETC1 for color textures without alpha + - BC5/ETC2 for normal maps + - BC4/EAC for single-channel textures (roughness, metallic, etc.) + +=== Summary and Next Steps + +In this chapter, we've explored the process of loading 3D models from glTF files and organizing them into a scene graph. We've covered: + +* The structure and advantages of the glTF format +* How to use the tinygltf library for efficient parsing +* The physically-based material system used in modern rendering +* How scene graphs organize objects in a hierarchical structure +* The representation of 3D geometry in meshes +* Animation systems for bringing models to life +* Integration with the rendering pipeline + +Our glTF loader creates a complete scene graph with: + +* Nodes organized in a hierarchy +* Meshes attached to nodes +* Materials defining surface properties +* Animations that can change node properties over time + +This structure allows us to: + +* Render complex 3D scenes +* Animate characters and objects +* Apply transformations that propagate through the hierarchy +* Optimize rendering for performance + +In the next chapter, we'll explore how to render these models using +physically-based rendering techniques, bringing our loaded assets to life +with realistic lighting and materials. + +link:03_model_system.adoc[Previous: Implementing the Model Loading System] | link:05_pbr_rendering.adoc[Next: Implementing PBR Rendering] diff --git a/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc new file mode 100644 index 00000000..e9cccceb --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/05_pbr_rendering.adoc @@ -0,0 +1,574 @@ +:pp: {plus}{plus} + += Loading Models: Implementing PBR for glTF Models + +== Applying PBR to glTF Models + +=== Building on PBR Knowledge + +In the xref:../Lighting_Materials/01_introduction.adoc[Lighting & Materials chapter], we explored the fundamentals of Physically Based Rendering (PBR), including its core principles, the BRDF, and material properties. Now, we'll apply that knowledge to implement a PBR pipeline for the glTF models we've loaded. + +As we learned in the link:../../15_GLTF_KTX2_Migration.html[glTF and KTX2 Migration chapter], glTF uses PBR with the metallic-roughness workflow for its material system. This aligns perfectly with the PBR concepts we've already covered, making it straightforward to render our glTF models with physically accurate lighting. + +=== Leveraging glTF's PBR Materials + +The glTF format already includes all the material properties we need for PBR: + +* *Base Color*: Defined by the baseColorFactor and baseColorTexture +* *Metallic and Roughness*: Defined by metallicFactor, roughnessFactor, and metallicRoughnessTexture +* *Normal Maps*: For surface detail without additional geometry +* *Occlusion Maps*: For approximating ambient occlusion +* *Emissive Maps*: For self-illuminating parts of the material + +By using these properties directly, we can ensure our rendering matches the artist's intent and produces physically accurate results. + +=== Implementing PBR in Our Engine + +Now that we understand the theory behind PBR, let's implement it in our engine. We'll build on the material data we loaded from glTF files in the previous chapter. + +==== Uniform Buffer for PBR + +We need to extend our uniform buffer to include PBR parameters: + +[source,cpp] +---- +// Structure for uniform buffer object +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; + + // PBR parameters + alignas(16) glm::vec4 lightPositions[4]; // Position and radius + alignas(16) glm::vec4 lightColors[4]; // RGB color and intensity + alignas(16) glm::vec4 camPos; // Camera position for view-dependent effects + alignas(4) float exposure = 4.5f; // Exposure for HDR rendering + alignas(4) float gamma = 2.2f; // Gamma correction value + alignas(4) float prefilteredCubeMipLevels = 1.0f; // For image-based lighting + alignas(4) float scaleIBLAmbient = 1.0f; // Scale factor for ambient lighting +}; +---- + +This uniform buffer includes: + +1. *Standard Transformation Matrices*: Model, view, and projection matrices for vertex transformation +2. *Light Information*: Positions and colors of up to four light sources +3. *Camera Position*: Needed for view-dependent effects like Fresnel +4. *Rendering Parameters*: Exposure, gamma, and other values for post-processing +5. *Image-Based Lighting Parameters*: For environment reflections (we'll cover this in a later chapter) + +==== Push Constants for Materials + +[NOTE] +==== +We introduced push constants earlier in xref:../Lighting_Materials/03_push_constants.adoc[push constants]; here we focus on how the same mechanism carries glTF metallic‑roughness material knobs efficiently per draw. +==== + +We'll use link:https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#descriptorsets-pushconstant[push constants] to pass material properties to the shader: + +[source,cpp] +---- +// Structure for push constants +struct PushConstantBlock { + glm::vec4 baseColorFactor; // RGB base color and alpha + float metallicFactor; // How metallic the surface is + float roughnessFactor; // How rough the surface is + int baseColorTextureSet; // Texture coordinate set for base color + int physicalDescriptorTextureSet; // Texture coordinate set for metallic-roughness + int normalTextureSet; // Texture coordinate set for normal map + int occlusionTextureSet; // Texture coordinate set for occlusion + int emissiveTextureSet; // Texture coordinate set for emission + float alphaMask; // Whether to use alpha masking + float alphaMaskCutoff; // Alpha threshold for masking +}; +---- + +Push constants are ideal for material properties because: + +* They can be updated quickly between draw calls +* They don't require descriptor sets +* They're perfect for per-object data like material properties + +==== Setting Up the Descriptor Sets + +To implement PBR, we need to set up descriptor sets for our textures and uniform buffer: + +[source,cpp] +---- +// Create descriptor set layout +void createDescriptorSetLayout() { + // Binding for uniform buffer + vk::DescriptorSetLayoutBinding uboBinding{ + .binding = 0, + .descriptorType = vk::DescriptorType::eUniformBuffer, + .descriptorCount = 1, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment + }; + + // Bindings for textures + std::array textureBindings{}; + + // Base color texture + textureBindings[0].binding = 1; + textureBindings[0].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[0].descriptorCount = 1; + textureBindings[0].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Metallic-roughness texture + textureBindings[1].binding = 2; + textureBindings[1].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[1].descriptorCount = 1; + textureBindings[1].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Normal map + textureBindings[2].binding = 3; + textureBindings[2].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[2].descriptorCount = 1; + textureBindings[2].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Occlusion map + textureBindings[3].binding = 4; + textureBindings[3].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[3].descriptorCount = 1; + textureBindings[3].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Emissive map + textureBindings[4].binding = 5; + textureBindings[4].descriptorType = vk::DescriptorType::eCombinedImageSampler; + textureBindings[4].descriptorCount = 1; + textureBindings[4].stageFlags = vk::ShaderStageFlagBits::eFragment; + + // Combine all bindings + std::array bindings = { + uboBinding, + textureBindings[0], + textureBindings[1], + textureBindings[2], + textureBindings[3], + textureBindings[4] + }; + + // Create the descriptor set layout + vk::DescriptorSetLayoutCreateInfo layoutInfo{ + .bindingCount = static_cast(bindings.size()), + .pBindings = bindings.data() + }; + + descriptorSetLayout = vk::raii::DescriptorSetLayout(device, layoutInfo); +} +---- + +==== Setting Up the Pipeline + +Our PBR pipeline needs to be configured for the specific requirements of physically-based rendering: + +[source,cpp] +---- +void createPipeline() { + // ... (standard pipeline setup code) + + // Enable alpha blending + 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 + }; + + // Set up push constants for material properties + vk::PushConstantRange pushConstantRange{ + .stageFlags = vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(PushConstantBlock) + }; + + // Create the pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutInfo{ + .setLayoutCount = 1, + .pSetLayouts = &descriptorSetLayout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &pushConstantRange + }; + + pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutInfo); + + // ... (rest of pipeline creation) +} +---- + +=== PBR Shader Implementation + +The heart of our PBR implementation is in the fragment shader. Here's a simplified version of a PBR fragment shader written in Slang: + +[source,slang] +---- + +// Input from vertex shader +struct VSOutput { + float3 WorldPos : POSITION; // Automatically assigned to location 0 + float3 Normal : NORMAL; // Automatically assigned to location 1 + float2 UV : TEXCOORD0; // Automatically assigned to location 2 + float4 Tangent : TANGENT; // Automatically assigned to location 3 +}; + +// Uniform buffer +struct UniformBufferObject { + float4x4 model; + float4x4 view; + float4x4 proj; + float4 lightPositions[4]; + float4 lightColors[4]; + float4 camPos; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; +}; + +// 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; +}; + +// Constants +static const float PI = 3.14159265359; + +// Bindings +ConstantBuffer ubo; +Texture2D baseColorMap; +SamplerState baseColorSampler; +Texture2D metallicRoughnessMap; +SamplerState metallicRoughnessSampler; +Texture2D normalMap; +SamplerState normalSampler; +Texture2D occlusionMap; +SamplerState occlusionSampler; +Texture2D emissiveMap; +SamplerState emissiveSampler; + +[[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 / denom; +} + +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(1.0 - cosTheta, 5.0); +} + +// Main fragment shader function +float4 main(VSOutput input) : SV_TARGET +{ + // Sample material textures + float4 baseColor = baseColorMap.Sample(baseColorSampler, input.UV) * material.baseColorFactor; + float2 metallicRoughness = metallicRoughnessMap.Sample(metallicRoughnessSampler, input.UV).bg; + float metallic = metallicRoughness.x * material.metallicFactor; + float roughness = metallicRoughness.y * material.roughnessFactor; + float ao = occlusionMap.Sample(occlusionSampler, input.UV).r; // link:https://learnopengl.com/Advanced-Lighting/SSAO[Ambient occlusion] + float3 emissive = emissiveMap.Sample(emissiveSampler, input.UV).rgb; // link:https://learnopengl.com/PBR/Lighting[Emissive lighting] (self-illumination) + + // Calculate normal in link:https://learnopengl.com/Advanced-Lighting/Normal-Mapping[tangent space] + float3 N = normalize(input.Normal); + if (material.normalTextureSet >= 0) { + // Apply link:https://learnopengl.com/Advanced-Lighting/Normal-Mapping[normal mapping] + float3 tangentNormal = normalMap.Sample(normalSampler, input.UV).xyz * 2.0 - 1.0; + float3 T = normalize(input.Tangent.xyz); + float3 B = normalize(cross(N, T)) * input.Tangent.w; + float3x3 TBN = float3x3(T, B, N); + N = normalize(mul(tangentNormal, TBN)); + } + + // Calculate view and reflection vectors + float3 V = normalize(ubo.camPos.xyz - input.WorldPos); + float3 R = reflect(-V, N); + + // Calculate F0 (base reflectivity) + float3 F0 = float3(0.04, 0.04, 0.04); + F0 = lerp(F0, baseColor.rgb, metallic); + + // Initialize lighting + float3 Lo = float3(0.0, 0.0, 0.0); + + // Calculate lighting for each light + for (int i = 0; i < 4; i++) { + float3 lightPos = ubo.lightPositions[i].xyz; + float3 lightColor = ubo.lightColors[i].rgb; + + // Calculate light direction and distance + float3 L = normalize(lightPos - input.WorldPos); + float distance = length(lightPos - input.WorldPos); + float attenuation = 1.0 / (distance * distance); + float3 radiance = lightColor * attenuation; + + // Calculate half vector (the normalized vector halfway between view and light direction) + // Used in link:https://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_reflection_model[Blinn-Phong] and PBR models + float3 H = normalize(V + L); + + // Calculate BRDF terms + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 0.0); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + // Specular BRDF + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 numerator = D * G * F; + float denominator = 4.0 * NdotV * NdotL + 0.0001; + float3 specular = numerator / denominator; + + // link:https://learnopengl.com/PBR/Theory[Energy conservation] + float3 kS = F; + float3 kD = float3(1.0, 1.0, 1.0) - kS; + kD *= 1.0 - metallic; + + // Add to outgoing radiance + Lo += (kD * baseColor.rgb / PI + specular) * radiance * NdotL; + } + + // Add ambient and emissive + float3 ambient = float3(0.03, 0.03, 0.03) * baseColor.rgb * ao; + float3 color = ambient + Lo + emissive; + + // link:https://en.wikipedia.org/wiki/High-dynamic-range_rendering[HDR] link:https://en.wikipedia.org/wiki/Tone_mapping[tonemapping] and link:https://en.wikipedia.org/wiki/Gamma_correction[gamma correction] + color = color / (color + float3(1.0, 1.0, 1.0)); + color = pow(color, float3(1.0 / ubo.gamma, 1.0 / ubo.gamma, 1.0 / ubo.gamma)); + + return float4(color, baseColor.a); +} +---- + +This shader implements the core PBR lighting model, including: + +* Sampling material textures +* Calculating normal mapping +* Computing the specular BRDF with D, F, and G terms +* Applying energy conservation +* Handling multiple light sources +* Tone mapping and gamma correction + +==== Lighting Setup for PBR + +PBR requires careful setup of light sources to achieve realistic results. Here's how we can set up lights in our application: + +[source,cpp] +---- +void setupLights() { + // Set up four lights with different positions and colors + std::array lightPositions = { + glm::vec4(-10.0f, 10.0f, 10.0f, 1.0f), + glm::vec4(10.0f, 10.0f, 10.0f, 1.0f), + glm::vec4(-10.0f, -10.0f, 10.0f, 1.0f), + glm::vec4(10.0f, -10.0f, 10.0f, 1.0f) + }; + + std::array lightColors = { + glm::vec4(300.0f, 300.0f, 300.0f, 1.0f), // White + glm::vec4(300.0f, 300.0f, 0.0f, 1.0f), // Yellow + glm::vec4(0.0f, 0.0f, 300.0f, 1.0f), // Blue + glm::vec4(300.0f, 0.0f, 0.0f, 1.0f) // Red + }; + + // Update uniform buffer with light data + for (size_t i = 0; i < maxConcurrentFrames; i++) { + UniformBufferObject ubo{}; + // ... (set up transformation matrices) + + // Set light positions and colors + for (int j = 0; j < 4; j++) { + ubo.lightPositions[j] = lightPositions[j]; + ubo.lightColors[j] = lightColors[j]; + } + + // Set camera position for view-dependent effects + ubo.camPos = glm::vec4(camera.getPosition(), 1.0f); + + // Set other PBR parameters + ubo.exposure = 4.5f; + ubo.gamma = 2.2f; + + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[i].mapped, &ubo, sizeof(ubo)); + } +} +---- + +==== Camera Integration for PBR + +PBR relies on view-dependent effects like the Fresnel effect, so we need to integrate our camera system: + +[source,cpp] +---- +void updateUniformBuffer(uint32_t currentFrame) { + UniformBufferObject ubo{}; + + // Update transformation matrices + ubo.model = glm::mat4(1.0f); // Or get from the model's node + ubo.view = camera.getViewMatrix(); + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + + // Vulkan's Y coordinate is inverted compared to OpenGL + ubo.proj[1][1] *= -1; + + // Update camera position for PBR calculations + ubo.camPos = glm::vec4(camera.getPosition(), 1.0f); + + // ... (update other PBR parameters) + + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); +} +---- + +=== Rendering with PBR + +Finally, let's put it all together to render our models with PBR: + +[source,cpp] +---- +void drawModel(vk::raii::CommandBuffer& commandBuffer, Model* model) { + // Bind descriptor set with uniform buffer and textures + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + 1, + &descriptorSets[currentFrame], + 0, + nullptr + ); + + // Traverse the model's scene graph + for (auto& node : model->linearNodes) { + if (node->mesh.indices.size() > 0) { + // Get the global transformation matrix + glm::mat4 nodeMatrix = node->getGlobalMatrix(); + + // Update model matrix in uniform buffer + // (In a real implementation, we'd use a separate UBO for each model) + + // Set up push constants for material properties + if (node->mesh.materialIndex >= 0) { + Material& mat = model->materials[node->mesh.materialIndex]; + + PushConstantBlock pushConstants{}; + pushConstants.baseColorFactor = mat.baseColorFactor; + pushConstants.metallicFactor = mat.metallicFactor; + pushConstants.roughnessFactor = mat.roughnessFactor; + pushConstants.baseColorTextureSet = mat.baseColorTextureIndex; + pushConstants.physicalDescriptorTextureSet = mat.metallicRoughnessTextureIndex; + pushConstants.normalTextureSet = mat.normalTextureIndex; + pushConstants.occlusionTextureSet = mat.occlusionTextureIndex; + pushConstants.emissiveTextureSet = mat.emissiveTextureIndex; + + commandBuffer.pushConstants( + pipelineLayout, + vk::ShaderStageFlagBits::eFragment, + 0, + sizeof(PushConstantBlock), + &pushConstants + ); + } + + // Bind vertex and index buffers + vk::Buffer vertexBuffers[] = {*node->mesh.vertexBuffer}; + vk::DeviceSize offsets[] = {0}; + commandBuffer.bindVertexBuffers(0, 1, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(*node->mesh.indexBuffer, 0, vk::IndexType::eUint32); + + // Draw the mesh + commandBuffer.drawIndexed( + static_cast(node->mesh.indices.size()), + 1, + 0, + 0, + 0 + ); + } + } +} +---- + +=== Advanced PBR Techniques + +While we've covered the basics of PBR implementation, there are several advanced techniques that can enhance the realism of your rendering: + +==== Image-Based Lighting (IBL) + +link:https://learnopengl.com/PBR/IBL/Diffuse-irradiance[IBL] uses environment maps to simulate global illumination: +* *Diffuse IBL*: Uses link:https://learnopengl.com/PBR/IBL/Diffuse-irradiance[irradiance maps] for ambient lighting +* *Specular IBL*: Uses link:https://learnopengl.com/PBR/IBL/Specular-IBL[pre-filtered environment maps] and link:https://learnopengl.com/PBR/IBL/Specular-IBL[BRDF integration maps] for reflections + +==== Subsurface Scattering + +For materials like skin, wax, or marble where light penetrates the surface: +* link:https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-14-advanced-techniques-realistic-real-time-skin[Simulates how light scatters within translucent materials] +* Can be approximated with techniques like subsurface scattering profiles + +==== Clear Coat + +For materials with a thin, glossy layer on top: +* link:https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat[Automotive paint, varnished wood, etc.] +* Implemented as an additional specular lobe + +==== Anisotropy + +For materials with directional reflections: +* link:https://google.github.io/filament/Filament.html#materialsystem/anisotropicmodel[Brushed metal, hair, fabric, etc.] +* Requires additional material parameters and modified BRDFs + +=== Conclusion and Next Steps + +In this chapter, we've applied the PBR knowledge from the Lighting & Materials chapter to implement a PBR pipeline for our glTF models. We've learned: + +* How to leverage the material properties from glTF for PBR rendering +* How to set up uniform buffers and push constants for PBR parameters +* How to implement a PBR shader that works with glTF materials +* How to integrate our camera system with PBR for view-dependent effects +* How to render glTF models with physically accurate lighting + +This implementation allows us to render the glTF models we loaded in the previous chapter with physically accurate materials, resulting in more realistic and consistent rendering across different lighting conditions. + +In the next chapter, we'll explore how to render multiple objects with different transformations, which will allow us to create more complex scenes with our PBR-enabled engine. + +If you want to dive deeper into lighting and materials, refer back to the Lighting & Materials chapter, where we explored the theory behind PBR in detail. + +xref:04_loading_gltf.adoc[Previous: Loading a glTF Model] | xref:06_multiple_objects.adoc[Next: Rendering Multiple Objects] diff --git a/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc b/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc new file mode 100644 index 00000000..c5942ec3 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/06_multiple_objects.adoc @@ -0,0 +1,787 @@ +:pp: {plus}{plus} + += Loading Models: Managing Multiple Objects + +== Managing Multiple Objects in a 3D Scene + +=== Introduction to Multi-Object Rendering + +In previous chapters, we've focused on loading and rendering a single 3D model. However, real-world applications rarely display just one object. Games, simulations, and visualizations typically contain many objects that interact within a shared environment. This chapter explores how to efficiently manage and render multiple objects in a 3D scene. + +The ability to render multiple objects is fundamental to creating rich, interactive environments. It involves not just duplicating models, but also managing their unique properties, spatial relationships, and rendering states. As we'll see, this introduces both challenges and opportunities for optimization. + +=== Approaches to Managing Multiple Objects + +There are several strategies for handling multiple objects in a 3D engine, each with different trade-offs: + +==== Object Instances vs. Multiple Models + +When creating a scene with multiple similar objects (like trees in a forest or buildings in a city), we have two main approaches: + +* *Multiple Model Instances*: Load the model once but render it multiple times with different transformations + - Advantages: Memory efficient, single asset to manage + - Use cases: Repeated elements like trees, rocks, furniture + +* *Unique Models*: Load separate models for each unique object + - Advantages: Greater variety, independent modifications + - Use cases: Main characters, unique structures, varied elements + +For our engine, we'll implement the instancing approach, which is more memory-efficient and suitable for many common scenarios. + +==== Scene Organization Strategies + +Beyond simply having multiple objects, we need to organize them effectively: + +* *Flat Collection*: Store all objects in a simple list or array + - Advantages: Simplicity, easy iteration + - Disadvantages: No spatial relationships, inefficient for large scenes + +* *Spatial Partitioning*: Organize objects by their location in 3D space + - Advantages: Efficient culling and queries, better performance for large scenes + - Examples: Octrees, BSP trees, grid systems + +* *Scene Graph*: Organize objects in a hierarchical tree structure + - Advantages: Parent-child relationships, hierarchical transformations + - Use cases: Articulated models, complex object relationships + +Our implementation will use a simple collection for this example, but in a more advanced engine, you would typically combine this with spatial partitioning and scene graph techniques. + +=== Performance Considerations + +Rendering multiple objects efficiently requires careful attention to performance: + +==== Draw Call Optimization + +Each object typically requires at least one draw call, which can become a bottleneck: + +* *Batching*: Combining similar objects into a single draw call +* *Instanced Rendering*: Using hardware instancing to draw multiple copies of the same mesh +* *Level of Detail (LOD)*: Using simpler models for distant objects + +==== Culling Techniques + +Not all objects need to be rendered every frame: + +* *Frustum Culling*: Skip rendering objects outside the camera's view +* *Occlusion Culling*: Skip rendering objects hidden behind other objects +* *Distance Culling*: Skip rendering objects too far from the camera + +==== Memory Management + +With multiple objects, memory usage becomes more critical: + +* *Shared Resources*: Reuse meshes, textures, and materials across objects +* *Asset Streaming*: Load and unload assets based on proximity to the camera +* *Instance Data*: Store only transformation and material variations per instance + +=== Implementing Object Instances + +Now let's implement a system for managing multiple object instances. We'll start with a simple structure to store instance data: + +[source,cpp] +---- +// Object instances - using the same structure as in our model system +struct ObjectInstance { + glm::vec3 position; // Position in world space + glm::vec3 rotation; // Rotation in Euler angles (degrees) + glm::vec3 scale; // Scale factors for each axis +}; + +// Collection of object instances +std::vector objectInstances; +---- + +This structure stores the position, rotation, and scale for each instance, along with a method to compute the model matrix. The model matrix transforms the object from its local space to world space, combining all three transformations. + +Next, we'll set up several instances with different transformations: + +[source,cpp] +---- +void setupObjectInstances() { + // Create multiple instances of the model with different positions + const int MAX_OBJECTS = 10; // Define how many objects we want + objectInstances.resize(MAX_OBJECTS); + + // Instance 1 - Center + objectInstances[0].position = glm::vec3(0.0f, 0.0f, 0.0f); + objectInstances[0].rotation = glm::vec3(0.0f, 0.0f, 0.0f); + objectInstances[0].scale = glm::vec3(1.0f); + + // Instance 2 - Left + objectInstances[1].position = glm::vec3(-2.0f, 0.0f, -1.0f); + objectInstances[1].rotation = glm::vec3(0.0f, 45.0f, 0.0f); + objectInstances[1].scale = glm::vec3(0.8f); + + // Instance 3 - Right + objectInstances[2].position = glm::vec3(2.0f, 0.0f, -1.0f); + objectInstances[2].rotation = glm::vec3(0.0f, -45.0f, 0.0f); + objectInstances[2].scale = glm::vec3(0.8f); + + // Instance 4 - Back Left + objectInstances[3].position = glm::vec3(-1.5f, 0.0f, -3.0f); + objectInstances[3].rotation = glm::vec3(0.0f, 30.0f, 0.0f); + objectInstances[3].scale = glm::vec3(0.7f); + + // Instance 5 - Back Right + objectInstances[4].position = glm::vec3(1.5f, 0.0f, -3.0f); + objectInstances[4].rotation = glm::vec3(0.0f, -30.0f, 0.0f); + objectInstances[4].scale = glm::vec3(0.7f); + + // Instance 6 - Front Left + objectInstances[5].position = glm::vec3(-1.5f, 0.0f, 1.5f); + objectInstances[5].rotation = glm::vec3(0.0f, -30.0f, 0.0f); + objectInstances[5].scale = glm::vec3(0.6f); + + // Instance 7 - Front Right + objectInstances[6].position = glm::vec3(1.5f, 0.0f, 1.5f); + objectInstances[6].rotation = glm::vec3(0.0f, 30.0f, 0.0f); + objectInstances[6].scale = glm::vec3(0.6f); + + // Instance 8 - Above + objectInstances[7].position = glm::vec3(0.0f, 2.0f, -2.0f); + objectInstances[7].rotation = glm::vec3(45.0f, 0.0f, 0.0f); + objectInstances[7].scale = glm::vec3(0.5f); + + // Instance 9 - Below + objectInstances[8].position = glm::vec3(0.0f, -1.0f, -2.0f); + objectInstances[8].rotation = glm::vec3(-30.0f, 0.0f, 0.0f); + objectInstances[8].scale = glm::vec3(0.5f); + + // Instance 10 - Far Back + objectInstances[9].position = glm::vec3(0.0f, 0.5f, -5.0f); + objectInstances[9].rotation = glm::vec3(0.0f, 180.0f, 0.0f); + objectInstances[9].scale = glm::vec3(1.2f); +} +---- + +This function creates ten instances of our model, each with a unique position, rotation, and scale. This allows us to create a more interesting scene with varied object placements. + +=== Rendering Multiple Objects + +Now that we have our object instances set up, we need to render them. Here's how we can modify our rendering loop to handle multiple objects: + +[source,cpp] +---- +void drawFrame() { + // ... (standard Vulkan frame setup) + + // Begin command buffer recording + commandBuffer.begin({}); + + // Transition image layout for rendering + transition_image_layout( + imageIndex, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eColorAttachmentOptimal, + {}, + vk::AccessFlagBits2::eColorAttachmentWrite, + vk::PipelineStageFlagBits2::eTopOfPipe, + vk::PipelineStageFlagBits2::eColorAttachmentOutput + ); + + // Set up rendering attachments + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + + vk::RenderingAttachmentInfo colorAttachmentInfo = { + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachmentInfo = { + .imageView = depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo = { + .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent }, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentInfo, + .pDepthAttachment = &depthAttachmentInfo + }; + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + + // Bind pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline); + + // Set viewport and scissor + commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); + commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + + // Bind descriptor set with uniform buffer and textures + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + 1, + &descriptorSets[currentFrame], + 0, + nullptr + ); + + // Update view and projection in uniform buffer + UniformBufferObject ubo{}; + ubo.view = camera.getViewMatrix(); + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted + + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); + + // Render each object instance + for (size_t i = 0; i < objectInstances.size(); i++) { + const auto& instance = objectInstances[i]; + + // Create model matrix for this instance + glm::mat4 modelMatrix = glm::mat4(1.0f); + modelMatrix = glm::translate(modelMatrix, instance.position); + modelMatrix = glm::rotate(modelMatrix, glm::radians(instance.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, glm::radians(instance.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, glm::radians(instance.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + modelMatrix = glm::scale(modelMatrix, instance.scale); + + // Render all nodes in the model + renderNode(commandBuffer, model.nodes, modelMatrix); + } + + // End dynamic rendering + commandBuffer.endRendering(); + + // Transition image layout for presentation + transition_image_layout( + imageIndex, + vk::ImageLayout::eColorAttachmentOptimal, + vk::ImageLayout::ePresentSrcKHR, + vk::AccessFlagBits2::eColorAttachmentWrite, + {}, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::PipelineStageFlagBits2::eBottomOfPipe + ); + + // End command buffer recording + commandBuffer.end(); + + // ... (submit command buffer and present) +} + +// Helper function to recursively render all nodes in the model +void renderNode(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // If this node has a mesh, render it + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { + + // Set up push constants for material properties + PushConstantBlock pushConstants{}; + + if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { + const auto& material = model.materials[node->mesh.materialIndex]; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; + pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; + } else { + // Default material properties + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 1.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 1; + pushConstants.physicalDescriptorTextureSet = -1; + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + } + + // Update model matrix in push constants + commandBuffer.pushConstants(pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants); + + // Bind vertex and index buffers + commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); + commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + + // Draw the mesh + commandBuffer.drawIndexed(static_cast(node->mesh.indices.size()), 1, 0, 0, 0); + } + + // Recursively render children + if (!node->children.empty()) { + renderNode(commandBuffer, node->children, nodeMatrix); + } + } +} +---- + +This rendering approach leverages our model system to efficiently render multiple instances of a model: + +1. It uses the scene graph structure to handle complex models with multiple parts +2. It properly handles parent-child relationships and hierarchical transformations +3. It applies material properties to each mesh using push constants +4. It supports animations through the node transformation system + +While this approach is more sophisticated than a simple flat list of objects, it does have some limitations: + +1. It still requires a separate draw call for each mesh in each instance, which can be inefficient for large numbers of objects +2. It doesn't implement any culling or batching optimizations +3. For very large scenes, additional spatial partitioning would be beneficial + +=== Advanced Techniques: Hardware Instancing + +For more efficient rendering of many similar objects, we can use hardware instancing. This allows us to draw multiple instances of the same model with a single draw call: + +[source,cpp] +---- +// Instance data for GPU instancing +struct InstanceData { + glm::mat4 model; // Model matrix for this instance +}; + +// Create buffers to hold instance data for each node with a mesh +std::vector instanceBuffers; +std::vector instanceBufferMemories; +std::vector instanceBuffersMapped; + +void setupInstanceBuffers() { + // Create an instance buffer for each node with a mesh + for (auto node : model.linearNodes) { + if (node->mesh.vertices.empty() || node->mesh.indices.empty()) { + continue; + } + + // Calculate buffer size + vk::DeviceSize bufferSize = sizeof(InstanceData) * objectInstances.size(); + + // Create the buffer + vk::raii::Buffer instanceBuffer = nullptr; + vk::raii::DeviceMemory instanceBufferMemory = nullptr; + createBuffer( + bufferSize, + vk::BufferUsageFlagBits::eVertexBuffer, + vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent, + instanceBuffer, + instanceBufferMemory + ); + + // Map the buffer memory + void* instanceBufferMapped = device.mapMemory(instanceBufferMemory, 0, bufferSize, {}); + + // Store buffer and memory + instanceBuffers.push_back(instanceBuffer); + instanceBufferMemories.push_back(instanceBufferMemory); + instanceBuffersMapped.push_back(instanceBufferMapped); + + // Set the instance buffer index for this node + node->instanceBufferIndex = static_cast(instanceBuffers.size() - 1); + } + + // Update all instance buffers + updateInstanceBuffers(); +} + +void updateInstanceBuffers() { + // For each node with an instance buffer + for (auto node : model.linearNodes) { + if (node->instanceBufferIndex < 0) { + continue; + } + + // Prepare instance data for this node + std::vector instanceData(objectInstances.size()); + for (size_t i = 0; i < objectInstances.size(); i++) { + // Create model matrix for this instance + glm::mat4 modelMatrix = glm::mat4(1.0f); + modelMatrix = glm::translate(modelMatrix, objectInstances[i].position); + modelMatrix = glm::rotate(modelMatrix, glm::radians(objectInstances[i].rotation.x), glm::vec3(1.0f, 0.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, glm::radians(objectInstances[i].rotation.y), glm::vec3(0.0f, 1.0f, 0.0f)); + modelMatrix = glm::rotate(modelMatrix, glm::radians(objectInstances[i].rotation.z), glm::vec3(0.0f, 0.0f, 1.0f)); + modelMatrix = glm::scale(modelMatrix, objectInstances[i].scale); + + // Combine with node's local matrix + instanceData[i].model = modelMatrix * node->getLocalMatrix(); + } + + // Copy to instance buffer + memcpy(instanceBuffersMapped[node->instanceBufferIndex], instanceData.data(), sizeof(InstanceData) * instanceData.size()); + } +} + +// Modify vertex input state to include instance data +vk::PipelineVertexInputStateCreateInfo vertexInputInfo{}; +// ... (standard vertex input setup) + +// Add instance data bindings and attributes +vk::VertexInputBindingDescription instanceBindingDescription{}; +instanceBindingDescription.binding = 1; // Use binding point 1 for instance data +instanceBindingDescription.stride = sizeof(InstanceData); +instanceBindingDescription.inputRate = vk::VertexInputRate::eInstance; // Advance per instance + +// Four attributes for the 4x4 matrix (one per row) +std::array instanceAttributeDescriptions{}; +for (uint32_t i = 0; i < 4; i++) { + instanceAttributeDescriptions[i].binding = 1; + instanceAttributeDescriptions[i].location = 4 + i; // Start after vertex attributes + instanceAttributeDescriptions[i].format = vk::Format::eR32G32B32A32Sfloat; + instanceAttributeDescriptions[i].offset = sizeof(float) * 4 * i; +} + +// Combine vertex and instance bindings/attributes +std::array bindingDescriptions = { + vertexBindingDescription, + instanceBindingDescription +}; + +std::vector attributeDescriptions; +// Add vertex attributes +for (const auto& attr : vertexAttributeDescriptions) { + attributeDescriptions.push_back(attr); +} +// Add instance attributes +for (const auto& attr : instanceAttributeDescriptions) { + attributeDescriptions.push_back(attr); +} + +// Update vertex input info +vertexInputInfo.vertexBindingDescriptionCount = static_cast(bindingDescriptions.size()); +vertexInputInfo.pVertexBindingDescriptions = bindingDescriptions.data(); +vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); +vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); +---- + +With hardware instancing set up, we can modify our rendering loop to draw all instances in a single call: + +The same five steps apply here; the difference is in step 4 where we bind the instance buffer and draw N instances: + +* Begin and describe attachments +* Begin rendering, bind pipeline, set viewport/scissor +* Update camera UBO (view/projection) +* Bind per‑mesh vertex + index buffers and a per‑mesh instance buffer, then draw instanced +* End rendering and present + +[source,cpp] +---- +void drawFrame() { + // ... (standard Vulkan frame setup) + + // Begin command buffer recording + commandBuffer.begin({}); + + // Transition image layout for rendering + transition_image_layout( + imageIndex, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eColorAttachmentOptimal, + {}, + vk::AccessFlagBits2::eColorAttachmentWrite, + vk::PipelineStageFlagBits2::eTopOfPipe, + vk::PipelineStageFlagBits2::eColorAttachmentOutput + ); + + // Set up rendering attachments + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + + vk::RenderingAttachmentInfo colorAttachmentInfo = { + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachmentInfo = { + .imageView = depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo = { + .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent }, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentInfo, + .pDepthAttachment = &depthAttachmentInfo + }; + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + + // Bind pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline); + + // Set viewport and scissor + commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); + commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + + // Update view and projection in uniform buffer + UniformBufferObject ubo{}; + ubo.view = camera.getViewMatrix(); + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted + + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); + + // Bind descriptor set + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + 1, + &descriptorSets[currentFrame], + 0, + nullptr + ); + + // Render all nodes in the model with instancing + renderNodeInstanced(commandBuffer, model.nodes); + + // End dynamic rendering + commandBuffer.endRendering(); + + // Transition image layout for presentation + transition_image_layout( + imageIndex, + vk::ImageLayout::eColorAttachmentOptimal, + vk::ImageLayout::ePresentSrcKHR, + vk::AccessFlagBits2::eColorAttachmentWrite, + {}, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::PipelineStageFlagBits2::eBottomOfPipe + ); + + // End command buffer recording + commandBuffer.end(); + + // ... (submit command buffer and present) +} + +// Helper function to recursively render all nodes in the model with instancing +void renderNodeInstanced(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes) { + for (const auto node : nodes) { + // If this node has a mesh and an instance buffer, render it + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0 && + node->instanceBufferIndex >= 0) { + + // Set up push constants for material properties + PushConstantBlock pushConstants{}; + + if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { + const auto& material = model.materials[node->mesh.materialIndex]; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; + pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; + } else { + // Default material properties + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 1.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 1; + pushConstants.physicalDescriptorTextureSet = -1; + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; + } + + // Update push constants + commandBuffer.pushConstants(pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants); + + // Bind vertex and instance buffers + vk::Buffer vertexBuffers[] = {*vertexBuffers[node->vertexBufferIndex], *instanceBuffers[node->instanceBufferIndex]}; + vk::DeviceSize offsets[] = {0, 0}; + commandBuffer.bindVertexBuffers(0, 2, vertexBuffers, offsets); + commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + + // Draw all instances of this mesh in a single call + commandBuffer.drawIndexed( + static_cast(node->mesh.indices.size()), + static_cast(objectInstances.size()), // Instance count + 0, 0, 0 + ); + } + + // Recursively render children + if (!node->children.empty()) { + renderNodeInstanced(commandBuffer, node->children); + } + } +} +---- + +This approach is much more efficient for rendering large numbers of similar objects, as it reduces the number of draw calls and uniform buffer updates. + +=== Vertex Shader Modifications for Instancing + +To support hardware instancing, we need to modify our vertex shader to use the instance data: + +[source,glsl] +---- +#version 450 + +// Vertex attributes +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec3 inColor; +layout(location = 3) in vec2 inTexCoord; + +// Instance attributes (model matrix, one row per attribute) +layout(location = 4) in vec4 instanceModelRow0; +layout(location = 5) in vec4 instanceModelRow1; +layout(location = 6) in vec4 instanceModelRow2; +layout(location = 7) in vec4 instanceModelRow3; + +// Uniform buffer for view and projection matrices +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; + + // PBR parameters (not used in this shader but included for compatibility) + vec4 lightPositions[4]; + vec4 lightColors[4]; + vec4 camPos; + float exposure; + float gamma; + float prefilteredCubeMipLevels; + float scaleIBLAmbient; +} ubo; + +// Output to fragment shader +layout(location = 0) out vec3 fragPosition; +layout(location = 1) out vec3 fragNormal; +layout(location = 2) out vec2 fragTexCoord; +layout(location = 3) out vec3 fragColor; + +void main() { + // Reconstruct model matrix from instance attributes + mat4 instanceModel = mat4( + instanceModelRow0, + instanceModelRow1, + instanceModelRow2, + instanceModelRow3 + ); + + // Calculate world position + vec4 worldPos = instanceModel * vec4(inPosition, 1.0); + + // Output position in clip space + gl_Position = ubo.proj * ubo.view * worldPos; + + // Pass data to fragment shader + fragPosition = worldPos.xyz; + fragNormal = mat3(instanceModel) * inNormal; // This is simplified; should use normal matrix + fragTexCoord = inTexCoord; + fragColor = inColor; +} +---- + +=== Beyond Basic Instancing: Material Variations + +So far, we've focused on positioning multiple instances of the same model with the same material. In a real application, you might want to vary the materials as well: + +[source,cpp] +---- +// Create materials with variations for each instance +void createMaterialVariations() { + // Resize the materials vector to hold one material per instance + model.materials.resize(objectInstances.size()); + + for (size_t i = 0; i < objectInstances.size(); i++) { + // Get reference to this instance's material + Material& material = model.materials[i]; + + // Vary materials based on position or other factors + float distanceFromCenter = glm::length(objectInstances[i].position); + float angle = atan2(objectInstances[i].position.z, objectInstances[i].position.x); + + // Vary color based on angle + float hue = (angle + glm::pi()) / (2.0f * glm::pi()); + glm::vec3 color = hsvToRgb(glm::vec3(hue, 0.7f, 0.9f)); + material.baseColorFactor = glm::vec4(color, 1.0f); + + // Vary metallic/roughness based on distance + material.metallicFactor = glm::clamp(distanceFromCenter / 5.0f, 0.0f, 1.0f); + material.roughnessFactor = glm::clamp(1.0f - distanceFromCenter / 5.0f, 0.1f, 0.9f); + + // Vary emissive strength for some objects + material.emissiveFactor = (i % 3 == 0) ? glm::vec3(1.0f) : glm::vec3(0.0f); // Every third object glows + } + + // Update material indices for all nodes + for (auto node : model.linearNodes) { + // For demonstration, we'll assign materials based on node index + // In a real application, you might use more sophisticated logic + if (!node->mesh.vertices.empty()) { + size_t materialIndex = node->index % objectInstances.size(); + node->mesh.materialIndex = static_cast(materialIndex); + } + } +} + +// Helper function to convert HSV to RGB +glm::vec3 hsvToRgb(glm::vec3 hsv) { + float h = hsv.x; + float s = hsv.y; + float v = hsv.z; + + float r, g, b; + + int i = floor(h * 6); + float f = h * 6 - i; + float p = v * (1 - s); + float q = v * (1 - f * s); + float t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + + return glm::vec3(r, g, b); +} + +// To use these material variations, call createMaterialVariations() after loading the model +// The renderNode() and renderNodeInstanced() methods will automatically use the assigned materials +---- + +This approach allows for much more visual variety in your scene, even when using the same base model for all instances. + +=== Conclusion and Next Steps + +In this chapter, we've explored how to manage and render multiple objects in a 3D scene. We've covered: + +* Different approaches to organizing multiple objects +* Performance considerations for multi-object rendering +* Basic implementation of object instances +* Advanced techniques like hardware instancing +* Material variations for visual diversity + +These techniques form the foundation for creating complex, visually rich 3D scenes. In the next chapter, we'll build upon this foundation to implement a complete scene rendering system that integrates all the components we've developed so far. + +link:05_pbr_rendering.adoc[Previous: Understanding Physically Based Rendering] | link:07_scene_rendering.adoc[Next: Rendering the Scene] diff --git a/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc b/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc new file mode 100644 index 00000000..0e0dacf1 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/07_scene_rendering.adoc @@ -0,0 +1,695 @@ +:pp: {plus}{plus} + += Loading Models: Rendering the Scene + +== Rendering the Scene + +=== Introduction to Scene Rendering + +Scene rendering is the process of transforming a 3D scene description into a 2D image that can be displayed on screen. In our engine, this involves traversing the scene graph, applying transformations, setting material properties, and issuing draw commands to the GPU. + +The scene rendering process is a critical part of the rendering pipeline, as it's where all the components we've built so far come together: + +* The model system provides the scene graph structure and mesh data +* The material system defines the appearance of objects +* The camera system determines the viewpoint +* The lighting system illuminates the scene + +In this chapter, we'll explore how these components work together to render a complete scene. + +=== Scene Graph Traversal + +A scene graph is a hierarchical tree structure that organizes objects in a scene. Each node in the tree can have a transformation (position, rotation, scale) and may contain a mesh to render. Nodes can also have child nodes, which inherit their parent's transformation. + +To render a scene graph, we need to traverse it in a depth-first manner, calculating the global transformation matrix for each node and rendering any meshes we encounter: + +[source,cpp] +---- +void renderScene(const vk::raii::CommandBuffer& commandBuffer, Model& model, const glm::mat4& viewMatrix, const glm::mat4& projectionMatrix) { + // Start traversal from the root nodes with an identity matrix + glm::mat4 rootMatrix = glm::mat4(1.0f); + renderNode(commandBuffer, model.nodes, rootMatrix); +} +---- + +The `renderNode` function is the heart of our scene rendering system. It recursively traverses the scene graph, calculating the global transformation matrix for each node and rendering any meshes it contains: + +=== Node traversal and transform calculation + +The rendering process begins with systematic traversal of the scene graph, where each node's transformation is calculated by combining its local transformation with its parent's accumulated transformation matrix. + +[source,cpp] +---- +void renderNode(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix) { + for (const auto node : nodes) { + // Calculate the cumulative transformation from root to current node + // This combines the parent's world transformation with this node's local transformation + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); +---- + +The transformation calculation represents the core of hierarchical scene graph rendering. Each node's `getLocalMatrix()` returns its transformation relative to its parent, which we then combine with the accumulated parent transformation using matrix multiplication. This mathematical operation effectively "chains" transformations down the hierarchy, ensuring that moving a parent node automatically moves all its children in world space. + +The order of multiplication is critical here: `parentMatrix * nodeLocalMatrix` ensures that the node's local transformation occurs first (in the node's local coordinate space), followed by the parent's transformation that places it in world space. This ordering preserves the hierarchical relationship where children are positioned relative to their parents. + +=== Mesh validation and rendering preparation + +Before rendering, we must validate that the node contains valid mesh data and has been properly uploaded to GPU buffers, ensuring robust rendering that handles incomplete or invalid scene graph nodes. + +[source,cpp] +---- + // Validate that this node has complete, renderable mesh data + // All conditions must be met for safe GPU rendering + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { +---- + +This validation step prevents rendering errors that could occur from incomplete scene graph nodes. Not every node in a scene graph necessarily contains renderable geometry - some nodes exist purely for organization or as transformation anchors for child objects. By checking for non-empty vertex and index arrays plus valid buffer indices, we ensure that we only attempt to render nodes that have been properly prepared with GPU resources. + +The buffer index checks (>= 0) are particularly important because they confirm that the mesh data has been successfully uploaded to GPU buffers and assigned valid indices in our buffer management system. Negative indices typically indicate uninitialized or failed buffer allocations. + +=== Material property configuration + +This material setup step translates high-level material descriptions into GPU-ready push constants that control the appearance and lighting properties of the rendered geometry. + +[source,cpp] +---- + // Initialize push constants structure for material data transfer + PushConstantBlock pushConstants{}; + + // Configure material properties if a valid material is assigned + if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { + const auto& material = model.materials[node->mesh.materialIndex]; + + // Set PBR material factors that control surface appearance + pushConstants.baseColorFactor = material.baseColorFactor; // Surface color tint + pushConstants.metallicFactor = material.metallicFactor; // Metallic vs. dielectric + pushConstants.roughnessFactor = material.roughnessFactor; // Surface roughness + + // Configure texture binding indices (-1 indicates no texture) + pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; + pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; + } else { + // Apply sensible default material properties for unassigned materials + pushConstants.baseColorFactor = glm::vec4(1.0f); // White base color + pushConstants.metallicFactor = 1.0f; // Fully metallic (safe default) + pushConstants.roughnessFactor = 1.0f; // Fully rough (safe default) + pushConstants.baseColorTextureSet = 1; // Assume default texture + pushConstants.physicalDescriptorTextureSet = -1; // No metallic/roughness texture + pushConstants.normalTextureSet = -1; // No normal map + pushConstants.occlusionTextureSet = -1; // No ambient occlusion + pushConstants.emissiveTextureSet = -1; // No emissive texture + } +---- + +The material configuration system bridges the gap between artist-authored materials and GPU shader parameters. Push constants provide the fastest path for updating per-object material data, as they bypass the GPU's memory hierarchy and are directly accessible to shader cores. This makes them ideal for material properties that change frequently between draw calls. + +The texture index mapping system (-1 for unused, positive integers for active bindings) allows shaders to conditionally sample textures based on availability. This approach provides flexibility where some materials might have normal maps while others don't, without requiring different shader variants or complex branching logic. + +The default material properties are chosen conservatively to prevent rendering artifacts when materials are missing or improperly configured. Metallic and roughness values of 1.0 tend to produce visually acceptable results across different lighting conditions, though they may not represent the intended material appearance. + +=== GPU resource binding and draw command execution + +The final rendering phase binds GPU resources and issues the actual draw command that transforms the scene graph node into rendered pixels on the screen. + +[source,cpp] +---- + // Upload material properties to GPU via push constants + // This provides fast, per-draw-call material parameter updates + commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eFragment, + 0, sizeof(PushConstantBlock), &pushConstants); + + // Bind geometry data buffers for GPU access + // Vertex buffer contains position, normal, texture coordinate data + commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); + // Index buffer defines triangle connectivity and enables vertex reuse + commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + + // Execute the draw command to render this mesh + // GPU processes indices to generate triangles and runs vertex/fragment shaders + commandBuffer.drawIndexed(static_cast(node->mesh.indices.size()), 1, 0, 0, 0); + } +---- + +The resource binding sequence follows Vulkan's explicit binding model where each resource type must be bound before use. Vertex buffers provide the per-vertex attribute data (positions, normals, texture coordinates), while index buffers define how vertices connect to form triangles. This indexed rendering approach reduces memory usage by allowing vertex reuse across multiple triangles. + +The `drawIndexed` command triggers GPU execution of the entire graphics pipeline for this mesh. The GPU processes each index to fetch vertex data, runs the vertex shader to transform geometry, rasterizes triangles to generate fragments, and executes the fragment shader to determine final pixel colors. All the material properties we configured via push constants become available to the fragment shader during this process. + +=== Hierarchical recursion + +Finally, ensure complete scene graph traversal by recursively processing child nodes with the accumulated transformation matrix, maintaining the hierarchical structure throughout the rendering process. + +[source,cpp] +---- + // Recursively process child nodes with accumulated transformation + // This maintains the hierarchical transformation chain down the scene graph + if (!node->children.empty()) { + renderNode(commandBuffer, node->children, nodeMatrix); + } + } +} +---- + +This traversal approach ensures that: + +1. Each node's transformation is correctly combined with its parent's transformation +2. Child nodes are rendered with the correct global transformation +3. The scene graph hierarchy is preserved during rendering + +=== Understanding the Rendering Process + +Let's break down the rendering process in more detail: + +==== Transformation Calculation + +The first step in rendering a node is calculating its global transformation matrix: + +[source,cpp] +---- +// Calculate global matrix for this node +glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); +---- + +This combines the node's local transformation (position, rotation, scale) with its parent's global transformation. The result is a matrix that transforms from the node's local space to world space. + +The `getLocalMatrix` method (defined in the `Node` class) combines the node's translation, rotation, and scale properties: + +[source,cpp] +---- +glm::mat4 getLocalMatrix() { + return glm::translate(glm::mat4(1.0f), translation) * + glm::toMat4(rotation) * + glm::scale(glm::mat4(1.0f), scale) * + matrix; +} +---- + +==== Material Setup + +[NOTE] +==== +We covered PBR material theory and shader details earlier in Loading_Models/05_pbr_rendering.adoc, so we won’t restate that here. This section focuses on the wiring: how material properties are packed into push constants and consumed by the draw call in this chapter’s context. +==== + +If the node has a mesh, we need to set up its material properties before rendering: + +[source,cpp] +---- +// Set up push constants for material properties +PushConstantBlock pushConstants{}; + +if (node->mesh.materialIndex >= 0 && node->mesh.materialIndex < static_cast(model.materials.size())) { + const auto& material = model.materials[node->mesh.materialIndex]; + pushConstants.baseColorFactor = material.baseColorFactor; + pushConstants.metallicFactor = material.metallicFactor; + pushConstants.roughnessFactor = material.roughnessFactor; + pushConstants.baseColorTextureSet = material.baseColorTextureIndex >= 0 ? 1 : -1; + pushConstants.physicalDescriptorTextureSet = material.metallicRoughnessTextureIndex >= 0 ? 2 : -1; + pushConstants.normalTextureSet = material.normalTextureIndex >= 0 ? 3 : -1; + pushConstants.occlusionTextureSet = material.occlusionTextureIndex >= 0 ? 4 : -1; + pushConstants.emissiveTextureSet = material.emissiveTextureIndex >= 0 ? 5 : -1; +} else { + // Default material properties + pushConstants.baseColorFactor = glm::vec4(1.0f); + pushConstants.metallicFactor = 1.0f; + pushConstants.roughnessFactor = 1.0f; + pushConstants.baseColorTextureSet = 1; + pushConstants.physicalDescriptorTextureSet = -1; + pushConstants.normalTextureSet = -1; + pushConstants.occlusionTextureSet = -1; + pushConstants.emissiveTextureSet = -1; +} + +// Update push constants +commandBuffer.pushConstants(*pipelineLayout, vk::ShaderStageFlagBits::eFragment, 0, sizeof(PushConstantBlock), &pushConstants); +---- + +This code: + +1. Retrieves the material associated with the mesh +2. Sets up push constants with the material properties +3. Passes these properties to the fragment shader using push constants + +The material properties include: + +* Base color factor (albedo) +* Metallic factor +* Roughness factor +* Texture set indices for various material maps (base color, metallic-roughness, normal, occlusion, emissive) + +==== Mesh Rendering + +Once the transformation and material are set up, we can render the mesh: + +[source,cpp] +---- +// Bind vertex and index buffers +commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); +commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + +// Draw the mesh +commandBuffer.drawIndexed(static_cast(node->mesh.indices.size()), 1, 0, 0, 0); +---- + +This code: + +1. Binds the vertex buffer containing the mesh's vertices +2. Binds the index buffer containing the mesh's indices +3. Issues a draw command to render the mesh + +==== Recursive Traversal + +After rendering the current node, we recursively traverse its children: + +[source,cpp] +---- +// Recursively render children +if (!node->children.empty()) { + renderNode(commandBuffer, node->children, nodeMatrix); +} +---- + +This ensures that all nodes in the scene graph are visited and rendered with the correct transformations. + +=== Integrating Scene Rendering in the Main Loop + +To use our scene rendering system in the main rendering loop, we need to set up the necessary Vulkan state and call the `renderScene` function. To keep this digestible, think of the frame as five steps: + +1) Begin and describe attachments (dynamic rendering inputs) +2) Begin rendering, bind pipeline, set viewport/scissor +3) Update camera UBO (view/projection) +4) Traverse scene graph and issue per-mesh draws +5) End rendering and present + +[source,cpp] +---- +void drawFrame() { + // ... (standard Vulkan frame setup) + + // Begin command buffer recording + commandBuffer.begin({}); + + // Transition image layout for rendering + transition_image_layout( + imageIndex, + vk::ImageLayout::eUndefined, + vk::ImageLayout::eColorAttachmentOptimal, + {}, + vk::AccessFlagBits2::eColorAttachmentWrite, + vk::PipelineStageFlagBits2::eTopOfPipe, + vk::PipelineStageFlagBits2::eColorAttachmentOutput + ); + + // Set up rendering attachments + vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f); + vk::ClearValue clearDepth = vk::ClearDepthStencilValue(1.0f, 0); + + vk::RenderingAttachmentInfo colorAttachmentInfo = { + .imageView = swapChainImageViews[imageIndex], + .imageLayout = vk::ImageLayout::eColorAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearColor + }; + + vk::RenderingAttachmentInfo depthAttachmentInfo = { + .imageView = depthImageView, + .imageLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .clearValue = clearDepth + }; + + vk::RenderingInfo renderingInfo = { + .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent }, + .layerCount = 1, + .colorAttachmentCount = 1, + .pColorAttachments = &colorAttachmentInfo, + .pDepthAttachment = &depthAttachmentInfo + }; + + // Begin dynamic rendering + commandBuffer.beginRendering(renderingInfo); + + // Bind pipeline + commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline); + + // Set viewport and scissor + commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast(swapChainExtent.width), static_cast(swapChainExtent.height), 0.0f, 1.0f)); + commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent)); + + // Bind descriptor set with uniform buffer and textures + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 0, + 1, + &descriptorSets[currentFrame], + 0, + nullptr + ); + + // Update view and projection in uniform buffer + UniformBufferObject ubo{}; + ubo.view = camera.getViewMatrix(); + ubo.proj = camera.getProjectionMatrix(swapChainExtent.width / (float)swapChainExtent.height); + ubo.proj[1][1] *= -1; // Vulkan's Y coordinate is inverted + + // Copy to uniform buffer (per frame-in-flight) + memcpy(uniformBuffers[currentFrame].mapped, &ubo, sizeof(ubo)); + + // Render the scene + renderScene(commandBuffer, model, ubo.view, ubo.proj); + + // End dynamic rendering + commandBuffer.endRendering(); + + // Transition image layout for presentation + transition_image_layout( + imageIndex, + vk::ImageLayout::eColorAttachmentOptimal, + vk::ImageLayout::ePresentSrcKHR, + vk::AccessFlagBits2::eColorAttachmentWrite, + {}, + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::PipelineStageFlagBits2::eBottomOfPipe + ); + + // End command buffer recording + commandBuffer.end(); + + // ... (submit command buffer and present) +} +---- + +This code: + +1. Sets up the Vulkan rendering state (command buffer, image transitions, rendering attachments) +2. Binds the graphics pipeline and descriptor sets +3. Updates the view and projection matrices in the uniform buffer +4. Calls `renderScene` to render the entire scene +5. Finalizes the rendering and presents the result + +=== Performance Considerations + +Rendering a complex scene can be performance-intensive. Here are some techniques to optimize scene rendering: + +==== Frustum Culling + +Frustum culling is the process of skipping the rendering of objects that are outside the camera's view frustum. This can significantly improve performance by reducing the number of draw calls: + +[source,cpp] +---- +bool isNodeVisible(const Node* node, const glm::mat4& viewProjection) { + // Calculate the node's bounding sphere in world space + glm::vec3 center = glm::vec3(node->getGlobalMatrix() * glm::vec4(node->boundingSphere.center, 1.0f)); + float radius = node->boundingSphere.radius * glm::length(glm::vec3(node->getGlobalMatrix()[0])); // Scale radius by the largest scale factor + + // Check if the bounding sphere is visible in the view frustum + for (int i = 0; i < 6; i++) { + // Extract frustum planes from the view-projection matrix + glm::vec4 plane = getFrustumPlane(viewProjection, i); + + // Calculate the signed distance from the sphere center to the plane + float distance = glm::dot(glm::vec4(center, 1.0f), plane); + + // If the sphere is completely behind the plane, it's not visible + if (distance < -radius) { + return false; + } + } + + return true; +} + +void renderNodeWithCulling(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix, const glm::mat4& viewProjection) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Check if the node is visible + if (isNodeVisible(node, viewProjection)) { + // Render the node (same as before) + // ... + + // Recursively render children + if (!node->children.empty()) { + renderNodeWithCulling(commandBuffer, node->children, nodeMatrix, viewProjection); + } + } + } +} +---- + +==== Level of Detail (LOD) + +Level of Detail (LOD) involves using simpler versions of models for objects that are far from the camera: + +[source,cpp] +---- +void renderNodeWithLOD(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix, const glm::vec3& cameraPosition) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Calculate distance to camera + glm::vec3 nodePosition = glm::vec3(nodeMatrix[3]); + float distanceToCamera = glm::distance(nodePosition, cameraPosition); + + // Select LOD level based on distance + int lodLevel = 0; + if (distanceToCamera > 50.0f) { + lodLevel = 2; // Low detail + } else if (distanceToCamera > 20.0f) { + lodLevel = 1; // Medium detail + } + + // Render the node with the selected LOD level + // ... + + // Recursively render children + if (!node->children.empty()) { + renderNodeWithLOD(commandBuffer, node->children, nodeMatrix, cameraPosition); + } + } +} +---- + +==== Occlusion Culling + +Occlusion culling involves skipping the rendering of objects that are hidden behind other objects: + +[source,cpp] +---- +void renderNodeWithOcclusion(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix) { + // Sort nodes by distance to camera (front to back) + std::vector> sortedNodes; + for (const auto node : nodes) { + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + glm::vec3 nodePosition = glm::vec3(nodeMatrix[3]); + float distanceToCamera = glm::length(nodePosition - cameraPosition); + sortedNodes.push_back({node, distanceToCamera}); + } + std::sort(sortedNodes.begin(), sortedNodes.end(), [](const auto& a, const auto& b) { + return a.second < b.second; + }); + + // Render nodes from front to back + for (const auto& [node, distance] : sortedNodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Begin occlusion query + vk::QueryPool occlusionQueryPool = createOcclusionQueryPool(); + commandBuffer.beginQuery(occlusionQueryPool, 0, {}); + + // Render the node's bounding box with depth write but no color write + renderBoundingBox(commandBuffer, node, nodeMatrix); + + // End occlusion query + commandBuffer.endQuery(occlusionQueryPool, 0); + + // Check if the node is visible + uint64_t occlusionResult = getOcclusionQueryResult(occlusionQueryPool); + if (occlusionResult > 0) { + // Node is visible, render it + // ... + + // Recursively render children + if (!node->children.empty()) { + renderNodeWithOcclusion(commandBuffer, node->children, nodeMatrix); + } + } + } +} +---- + +==== Instanced Rendering + +For scenes with many identical objects, instanced rendering can significantly improve performance: + +[source,cpp] +---- +void renderInstanced(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const std::vector& instanceMatrices) { + for (const auto node : nodes) { + // If this node has a mesh, render it with instancing + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { + + // Set up material properties (same as before) + // ... + + // Bind vertex and index buffers + commandBuffer.bindVertexBuffers(0, *vertexBuffers[node->vertexBufferIndex], {0}); + commandBuffer.bindIndexBuffer(*indexBuffers[node->indexBufferIndex], 0, vk::IndexType::eUint32); + + // Create and bind instance buffer + vk::raii::Buffer instanceBuffer = createInstanceBuffer(instanceMatrices); + commandBuffer.bindVertexBuffers(1, *instanceBuffer, {0}); + + // Draw the mesh with instancing + commandBuffer.drawIndexedInstanced( + static_cast(node->mesh.indices.size()), + static_cast(instanceMatrices.size()), + 0, 0, 0 + ); + } + + // Recursively render children + if (!node->children.empty()) { + renderInstanced(commandBuffer, node->children, instanceMatrices); + } + } +} +---- + +=== Advanced Scene Rendering Techniques + +Beyond basic scene rendering, there are several advanced techniques that can enhance the visual quality and performance of your renderer: + +==== Hierarchical Culling + +Hierarchical culling involves using the scene graph structure to accelerate culling operations: + +[source,cpp] +---- +bool isNodeAndChildrenVisible(const Node* node, const glm::mat4& viewProjection, const glm::mat4& parentMatrix) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Check if the node's bounding volume is visible + if (!isNodeVisible(node, viewProjection, nodeMatrix)) { + // If the node is not visible, none of its children are visible either + return false; + } + + // Node is visible, check if it has a mesh to render + bool hasVisibleContent = !node->mesh.vertices.empty() && !node->mesh.indices.empty(); + + // Recursively check children + for (const auto child : node->children) { + hasVisibleContent |= isNodeAndChildrenVisible(child, viewProjection, nodeMatrix); + } + + return hasVisibleContent; +} + +void renderNodeHierarchical(const vk::raii::CommandBuffer& commandBuffer, const std::vector& nodes, const glm::mat4& parentMatrix, const glm::mat4& viewProjection) { + for (const auto node : nodes) { + // Calculate global matrix for this node + glm::mat4 nodeMatrix = parentMatrix * node->getLocalMatrix(); + + // Check if the node and its children are visible + if (isNodeAndChildrenVisible(node, viewProjection, glm::mat4(1.0f))) { + // Render the node if it has a mesh + if (!node->mesh.vertices.empty() && !node->mesh.indices.empty() && + node->vertexBufferIndex >= 0 && node->indexBufferIndex >= 0) { + // Render the node (same as before) + // ... + } + + // Recursively render children + if (!node->children.empty()) { + renderNodeHierarchical(commandBuffer, node->children, nodeMatrix, viewProjection); + } + } + } +} +---- + +==== Deferred Rendering + +Deferred rendering separates the geometry and lighting passes, which can improve performance for scenes with many lights: + +[source,cpp] +---- +void renderSceneDeferred(const vk::raii::CommandBuffer& commandBuffer, Model& model) { + // Geometry pass: render scene to G-buffer + beginGeometryPass(commandBuffer); + renderNode(commandBuffer, model.nodes, glm::mat4(1.0f)); + endGeometryPass(commandBuffer); + + // Lighting pass: apply lighting to G-buffer + beginLightingPass(commandBuffer); + for (const auto& light : lights) { + renderLight(commandBuffer, light); + } + endLightingPass(commandBuffer); +} +---- + +==== Clustered Rendering + +Clustered rendering divides the view frustum into 3D cells to efficiently handle many lights: + +[source,cpp] +---- +void setupLightClusters() { + // Divide the view frustum into a 3D grid of clusters + const int clusterCountX = 16; + const int clusterCountY = 9; + const int clusterCountZ = 24; + + // Assign lights to clusters based on their position and radius + for (const auto& light : lights) { + for (int x = 0; x < clusterCountX; x++) { + for (int y = 0; y < clusterCountY; y++) { + for (int z = 0; z < clusterCountZ; z++) { + if (lightAffectsCluster(light, x, y, z)) { + lightClusters[x][y][z].push_back(light.index); + } + } + } + } + } + + // Upload light cluster data to GPU + updateLightClusterBuffer(); +} + +void renderSceneClustered(const vk::raii::CommandBuffer& commandBuffer, Model& model) { + // Bind light cluster buffer + commandBuffer.bindDescriptorSets( + vk::PipelineBindPoint::eGraphics, + pipelineLayout, + 1, + 1, + &lightClusterDescriptorSet, + 0, + nullptr + ); + + // Render scene normally + renderNode(commandBuffer, model.nodes, glm::mat4(1.0f)); +} +---- + +=== Conclusion + +In this chapter, we've explored the process of rendering a scene using a scene graph. We've seen how to traverse the scene graph, calculate transformations, apply materials, and render meshes. We've also discussed various optimization techniques to improve performance. + +The scene rendering system we've built is flexible and extensible, allowing for the rendering of complex scenes with multiple objects, materials, and lighting conditions. In the next chapter, we'll build on this foundation to implement animations, bringing our scenes to life. + +link:06_multiple_objects.adoc[Previous: Rendering Multiple Objects] | link:08_animations.adoc[Next: Updating Animations] diff --git a/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc b/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc new file mode 100644 index 00000000..a492e37f --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/08_animations.adoc @@ -0,0 +1,1061 @@ +:pp: {plus}{plus} + += Loading Models: Updating Animations + +== Understanding and Implementing Animations + +=== Introduction to 3D Animations + +Animation is a crucial aspect of modern 3D applications, bringing static models to life with movement and interactivity. In our engine, we've implemented a robust animation system that supports skeletal animations from glTF files. + +Animations in 3D graphics typically involve: + +* *Keyframes*: Specific points in time where the state of an object is explicitly defined +* *Interpolation*: The process of calculating intermediate states between keyframes +* *Channels*: Different properties that can be animated (position, rotation, scale) +* *Bones/Joints*: A hierarchical structure that defines how parts of a model move together + +glTF provides a standardized way to store and transfer animations, which our engine can load and play back. + +=== Animation Data Structures + +As we saw in the link:03_model_system.adoc[Model System chapter], our engine uses several structures to represent animations: + +[source,cpp] +---- +// Structure for animation keyframes +struct AnimationChannel { + enum PathType { TRANSLATION, ROTATION, SCALE }; + PathType path; + Node* node = nullptr; + uint32_t samplerIndex; +}; + +// Structure for animation interpolation +struct AnimationSampler { + enum InterpolationType { LINEAR, STEP, CUBICSPLINE }; + InterpolationType interpolation; + std::vector inputs; // Key frame timestamps + std::vector outputsVec4; // Key frame values (for rotations) + std::vector outputsVec3; // Key frame values (for translations and scales) +}; + +// Structure for animation +struct Animation { + std::string name; + std::vector samplers; + std::vector channels; + float start = std::numeric_limits::max(); + float end = std::numeric_limits::min(); + float currentTime = 0.0f; +}; +---- + +These structures work together to define how animations are stored and processed: + +* *Animation*: Contains multiple channels and samplers, representing a complete animation sequence +* *AnimationChannel*: Links a node in the scene graph to a specific animation property (translation, rotation, or scale) +* *AnimationSampler*: Defines how to interpolate between keyframes for a specific channel + +=== How Animation Playback Works + +The animation update process is the heart of our animation system, responsible for translating time-based animation data into actual transformations applied to scene graph nodes. + +=== Animation Update: Validation and Time Management + +Before we start anything, we should validate that we have valid animation data and manage the progression of animation time, including looping behavior for cyclical animations. + +[source,cpp] +---- +void Model::updateAnimation(uint32_t index, float deltaTime) { + // Validate animation data and index bounds + if (animations.empty() || index >= animations.size()) { + return; + } + + // Update animation timing with automatic looping + Animation& animation = animations[index]; + animation.currentTime += deltaTime; + if (animation.currentTime > animation.end) { + animation.currentTime = animation.start; + } +---- + +Animation validation is critical for robust systems because not all models contain animations, and external code might request non-existent animation indices. By performing this check early, we avoid crashes and undefined behavior when working with static models or invalid animation requests. This defensive programming approach is essential in production game engines where content from various sources might have inconsistent animation data. + +Time management forms the foundation of animation playback, where the deltaTime parameter represents the elapsed time since the last update. This frame-rate independent approach ensures animations play at consistent speeds regardless of rendering performance. The automatic looping mechanism seamlessly restarts animations when they reach their end time, creating continuous motion that's essential for idle animations, walking cycles, and other repetitive movements. + +=== Animation Update: Channel Iteration and Sampler Access + +New we iterate through all animation channels, establishing the connection between abstract animation data and the specific nodes in our scene graph that will receive transformation updates. + +[source,cpp] +---- + // Process each animation channel to update corresponding scene nodes + for (auto& channel : animation.channels) { + AnimationSampler& sampler = animation.samplers[channel.samplerIndex]; +---- + +The channel iteration represents the heart of our animation-to-scene-graph mapping system. Each channel defines a specific transformation type (position, rotation, or scale) for a particular node in the scene hierarchy. This one-to-many relationship allows complex animations where multiple properties of multiple nodes can be animated simultaneously, enabling sophisticated character animations with dozens of moving parts. + +The sampler access pattern demonstrates the separation of concerns in our animation architecture. Samplers contain the actual keyframe data and interpolation logic, while channels define what gets animated. This design allows multiple channels to share the same sampler data, reducing memory usage when the same animation curve applies to different nodes or when different transformation components follow identical patterns. + +=== Animation Update: Keyframe Location and Interpolation Factor Calculation + +Next, locate the appropriate keyframes that surround the current animation time and calculate the precise interpolation factor needed for smooth transitions between discrete animation samples. + +[source,cpp] +---- + // Find the current keyframe pair that brackets the animation time + for (size_t i = 0; i < sampler.inputs.size() - 1; i++) { + if (animation.currentTime >= sampler.inputs[i] && animation.currentTime <= sampler.inputs[i + 1]) { + // Calculate normalized interpolation factor between keyframes + float t = (animation.currentTime - sampler.inputs[i]) / (sampler.inputs[i + 1] - sampler.inputs[i]); +---- + +The keyframe search algorithm performs a linear scan to find the pair of keyframes that bracket the current animation time. While this approach has O(n) complexity, it's practical for typical animation data where keyframes are relatively sparse. Production systems often optimize this with binary search or by caching the last keyframe index, but the linear approach provides clarity for educational purposes and adequate performance for most real-world animation sequences. + +The interpolation factor calculation creates a normalized value between 0.0 and 1.0 that represents exactly where the current time falls between two keyframes. When t=0.0, we're at the first keyframe; when t=1.0, we're at the second keyframe; values in between create smooth transitions. This mathematical foundation enables all the interpolation techniques that follow, whether for linear position changes or complex quaternion rotations. + +=== Animation Update: Property-Specific Interpolation and Node Updates + +Finally, apply the appropriate mathematical interpolation technique based on the transformation type, updating the actual scene graph nodes with the computed animation values. + +[source,cpp] +---- + // Apply transformation based on the specific animation channel type + switch (channel.path) { + case AnimationChannel::TRANSLATION: { + // Linear interpolation for position changes + glm::vec3 start = sampler.outputsVec3[i]; + glm::vec3 end = sampler.outputsVec3[i + 1]; + channel.node->translation = glm::mix(start, end, t); + break; + } + case AnimationChannel::ROTATION: { + // Spherical linear interpolation for smooth rotation transitions + glm::quat start = glm::quat(sampler.outputsVec4[i].w, sampler.outputsVec4[i].x, sampler.outputsVec4[i].y, sampler.outputsVec4[i].z); + glm::quat end = glm::quat(sampler.outputsVec4[i + 1].w, sampler.outputsVec4[i + 1].x, sampler.outputsVec4[i + 1].y, sampler.outputsVec4[i + 1].z); + channel.node->rotation = glm::slerp(start, end, t); + break; + } + case AnimationChannel::SCALE: { + // Linear interpolation for scaling transformations + glm::vec3 start = sampler.outputsVec3[i]; + glm::vec3 end = sampler.outputsVec3[i + 1]; + channel.node->scale = glm::mix(start, end, t); + break; + } + } + break; + } + } + } +} +---- + +This method: + +1. Updates the animation's current time based on the delta time +2. Loops the animation if it reaches the end +3. For each channel in the animation: + a. Finds the current keyframe based on the current time + b. Calculates the interpolation factor between the current and next keyframe + c. Interpolates between keyframe values based on the channel type (translation, rotation, or scale) + d. Updates the corresponding node's transformation + +=== Integrating Animation Updates in the Main Loop + +To animate our models, we need to update the animation state each frame: + +[source,cpp] +---- +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + + // Update animation time + static auto lastTime = std::chrono::high_resolution_clock::now(); + auto currentTime = std::chrono::high_resolution_clock::now(); + float deltaTime = std::chrono::duration(currentTime - lastTime).count(); + lastTime = currentTime; + + // Update model animations + animationTime += deltaTime; + if (!model.animations.empty()) { + model.updateAnimation(0, deltaTime); + } + + drawFrame(); + } + + device.waitIdle(); +} +---- + +This code: + +1. Calculates the time elapsed since the last frame (deltaTime) +2. Updates a global animation time counter (useful for custom animations) +3. Calls `updateAnimation` on the model if it has animations +4. Renders the frame with the updated animation state + +=== Advanced Animation Techniques + +While our basic animation system handles most common use cases, there are several advanced techniques you might want to implement: + +==== Animation Blending + +Animation blending is a technique that combines multiple animations to create smooth transitions or entirely new animations. This is essential for creating realistic character movement and responsive gameplay. + +===== Understanding Animation Blending + +At its core, animation blending works by interpolating between the transformations (position, rotation, scale) of corresponding bones or nodes in different animations. The key concepts include: + +* *Blend Factor*: A value between 0.0 and 1.0 that determines how much of each animation contributes to the final result +* *Blend Space*: A multidimensional space where animations are positioned based on parameters (like speed, direction) +* *Blend Trees*: Hierarchical structures that organize multiple blends into complex animation systems + +===== Types of Animation Blending + +There are several common types of animation blending: + +* *Linear Blending*: Simple interpolation between two animations (e.g., transitioning from walk to run) +* *Additive Blending*: One animation is added on top of another (e.g., adding a "wounded" limp to any movement animation) +* *Partial Blending*: Blending that affects only certain parts of the skeleton (e.g., aiming a weapon while walking) +* *Parametric Blending*: Blending multiple animations based on continuous parameters (e.g., direction + speed) + +===== Implementing Basic Animation Blending + +Here's how to implement a simple linear blend between two animations: + +[source,cpp] +---- +void blendAnimations(uint32_t fromIndex, uint32_t toIndex, float blendFactor) { + // Store original node transformations + std::vector originalTranslations; + std::vector originalRotations; + std::vector originalScales; + + for (auto node : model.linearNodes) { + originalTranslations.push_back(node->translation); + originalRotations.push_back(node->rotation); + originalScales.push_back(node->scale); + } + + // Apply first animation fully + model.updateAnimation(fromIndex, 0.0f); + + // Store intermediate transformations + std::vector fromTranslations; + std::vector fromRotations; + std::vector fromScales; + + for (auto node : model.linearNodes) { + fromTranslations.push_back(node->translation); + fromRotations.push_back(node->rotation); + fromScales.push_back(node->scale); + } + + // Restore original transformations + for (size_t i = 0; i < model.linearNodes.size(); i++) { + model.linearNodes[i]->translation = originalTranslations[i]; + model.linearNodes[i]->rotation = originalRotations[i]; + model.linearNodes[i]->scale = originalScales[i]; + } + + // Apply second animation fully + model.updateAnimation(toIndex, 0.0f); + + // Blend between the two animations + for (size_t i = 0; i < model.linearNodes.size(); i++) { + model.linearNodes[i]->translation = glm::mix(fromTranslations[i], model.linearNodes[i]->translation, blendFactor); + model.linearNodes[i]->rotation = glm::slerp(fromRotations[i], model.linearNodes[i]->rotation, blendFactor); + model.linearNodes[i]->scale = glm::mix(fromScales[i], model.linearNodes[i]->scale, blendFactor); + } +} +---- + +This implementation: + +1. Captures the original state of all nodes +2. Applies the first animation and stores its transformations +3. Restores the original state +4. Applies the second animation +5. Blends between the two animations using linear interpolation for positions and scales, and spherical interpolation for rotations + +===== Advanced Blending Techniques + +For more complex scenarios, we can implement more sophisticated blending: + +[source,cpp] +---- +// Multi-way blending with weights +void blendMultipleAnimations(const std::vector& animationIndices, + const std::vector& weights) { + if (animationIndices.empty() || weights.empty() || + animationIndices.size() != weights.size()) { + return; + } + + // Normalize weights + float totalWeight = 0.0f; + for (float weight : weights) { + totalWeight += weight; + } + + std::vector> allTranslations; + std::vector> allRotations; + std::vector> allScales; + + // Store original transformations + std::vector originalTranslations; + std::vector originalRotations; + std::vector originalScales; + + for (auto node : model.linearNodes) { + originalTranslations.push_back(node->translation); + originalRotations.push_back(node->rotation); + originalScales.push_back(node->scale); + } + + // Collect transformations from all animations + for (uint32_t animIndex : animationIndices) { + // Reset to original state + for (size_t i = 0; i < model.linearNodes.size(); i++) { + model.linearNodes[i]->translation = originalTranslations[i]; + model.linearNodes[i]->rotation = originalRotations[i]; + model.linearNodes[i]->scale = originalScales[i]; + } + + // Apply this animation + model.updateAnimation(animIndex, 0.0f); + + // Store transformations + std::vector translations; + std::vector rotations; + std::vector scales; + + for (auto node : model.linearNodes) { + translations.push_back(node->translation); + rotations.push_back(node->rotation); + scales.push_back(node->scale); + } + + allTranslations.push_back(translations); + allRotations.push_back(rotations); + allScales.push_back(scales); + } + + // Reset to original state + for (size_t i = 0; i < model.linearNodes.size(); i++) { + model.linearNodes[i]->translation = originalTranslations[i]; + model.linearNodes[i]->rotation = originalRotations[i]; + model.linearNodes[i]->scale = originalScales[i]; + } + + // Apply weighted blend + for (size_t nodeIdx = 0; nodeIdx < model.linearNodes.size(); nodeIdx++) { + glm::vec3 blendedTranslation(0.0f); + glm::quat blendedRotation(0.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 blendedScale(0.0f); + + // First pass for translations and scales + for (size_t animIdx = 0; animIdx < animationIndices.size(); animIdx++) { + float normalizedWeight = weights[animIdx] / totalWeight; + blendedTranslation += allTranslations[animIdx][nodeIdx] * normalizedWeight; + blendedScale += allScales[animIdx][nodeIdx] * normalizedWeight; + } + + // Special handling for quaternions (rotations) + // We use nlerp (normalized lerp) for multiple quaternions + for (size_t animIdx = 0; animIdx < animationIndices.size(); animIdx++) { + float normalizedWeight = weights[animIdx] / totalWeight; + if (animIdx == 0) { + blendedRotation = allRotations[animIdx][nodeIdx] * normalizedWeight; + } else { + // Ensure we're interpolating along the shortest path + if (glm::dot(blendedRotation, allRotations[animIdx][nodeIdx]) < 0) { + blendedRotation += -allRotations[animIdx][nodeIdx] * normalizedWeight; + } else { + blendedRotation += allRotations[animIdx][nodeIdx] * normalizedWeight; + } + } + } + + // Normalize the resulting quaternion + blendedRotation = glm::normalize(blendedRotation); + + // Apply the blended transformations + model.linearNodes[nodeIdx]->translation = blendedTranslation; + model.linearNodes[nodeIdx]->rotation = blendedRotation; + model.linearNodes[nodeIdx]->scale = blendedScale; + } +} +---- + +This more advanced implementation allows for blending between any number of animations with different weights, which is essential for complex animation systems like locomotion or facial expressions. + +===== Blend Spaces + +For character movement, blend spaces are particularly useful. A blend space is a 2D or 3D space where animations are positioned based on parameters like speed and direction: + +[source,cpp] +---- +// Simple 2D blend space for locomotion (direction + speed) +struct BlendSpaceAnimation { + uint32_t animationIndex; + float directionAngle; // In degrees, 0 = forward, 180 = backward + float speed; // In units/second +}; + +void updateLocomotionBlendSpace(float currentDirection, float currentSpeed) { + // Define our blend space animations + std::vector blendSpace = { + {0, 0.0f, 0.0f}, // Idle + {1, 0.0f, 1.0f}, // Walk Forward + {2, 0.0f, 3.0f}, // Run Forward + {3, 90.0f, 1.0f}, // Walk Right + {4, 90.0f, 3.0f}, // Run Right + {5, 180.0f, 1.0f}, // Walk Backward + {6, 180.0f, 3.0f}, // Run Backward + {7, 270.0f, 1.0f}, // Walk Left + {8, 270.0f, 3.0f} // Run Left + }; + + // Find the closest animations and their weights + std::vector animIndices; + std::vector weights; + + // Normalize direction to 0-360 range + currentDirection = fmod(currentDirection + 360.0f, 360.0f); + + // Find the 3 closest animations in the blend space + // This is a simplified approach - a real implementation would use triangulation + for (const auto& anim : blendSpace) { + float distDir = std::min(std::abs(currentDirection - anim.directionAngle), + 360.0f - std::abs(currentDirection - anim.directionAngle)); + float distSpeed = std::abs(currentSpeed - anim.speed); + + // Calculate distance in blend space (weighted combination of direction and speed) + float distance = std::sqrt(distDir * distDir * 0.01f + distSpeed * distSpeed); + + // Use inverse distance weighting + if (distance < 0.001f) { + // If we're very close to an exact animation, just use that one + animIndices = {anim.animationIndex}; + weights = {1.0f}; + break; + } + + float weight = 1.0f / (distance + 0.1f); // Add small epsilon to avoid division by zero + animIndices.push_back(anim.animationIndex); + weights.push_back(weight); + + // Limit to 3 closest animations for performance + if (animIndices.size() > 3) { + // Find the smallest weight + auto minIt = std::min_element(weights.begin(), weights.end()); + size_t minIdx = std::distance(weights.begin(), minIt); + + // Remove the animation with the smallest weight + animIndices.erase(animIndices.begin() + minIdx); + weights.erase(weights.begin() + minIdx); + } + } + + // Blend the selected animations + blendMultipleAnimations(animIndices, weights); +} +---- + +This blend space implementation allows for smooth transitions between different movement animations based on the character's current direction and speed. + +While animation blending gives us powerful tools to combine pre-created animations, sometimes we need to adapt animations to dynamic environments in real-time. For example, how do we make a character's hand precisely grab an object, or ensure feet properly plant on uneven terrain? This is where our next technique comes in. + +==== Inverse Kinematics (IK) + +Inverse Kinematics complements our animation system by allowing procedural adjustments to character poses. While the animation playback we implemented earlier uses Forward Kinematics (calculating positions from rotations), IK works in reverse - determining the joint rotations needed to achieve a specific end position. + +===== Forward vs. Inverse Kinematics + +To understand IK, it helps to contrast it with Forward Kinematics: + +* *Forward Kinematics (FK)*: Given joint angles, calculate the position of the end effector + - Straightforward to compute + - Predictable and stable + - Used in most animation playback + +* *Inverse Kinematics (IK)*: Given a desired end effector position, calculate the joint angles + - More complex to compute + - May have multiple solutions or no solution + - Essential for adaptive animations and interactions + +===== Common IK Applications + +Just as we use animation blending to create smooth transitions between predefined animations, we use IK to adapt those animations to dynamic environments. IK enhances our animation system in several key ways: + +* *Foot Placement*: Remember how our animations update node transformations? With IK, we can adjust those transformations to ensure feet properly contact uneven terrain, preventing the "floating feet" problem common in games +* *Hand Placement*: Similar to our blend space example where we interpolate between different animations, IK lets us precisely position a character's hands to grab objects at any position +* *Aiming*: We can use IK to orient a character's upper body toward a target while the lower body follows a different animation +* *Procedural Animation*: IK allows us to generate new animations on-the-fly based on environmental constraints +* *Ragdoll Physics*: When transitioning from animated to physics-driven movement (like when a character falls), IK helps create realistic physical responses + +===== IK Algorithms + +Just as we have different interpolation methods for animation keyframes (LINEAR, STEP, CUBICSPLINE in our AnimationSampler), we have different algorithms for solving IK problems: + +* *Analytical Methods*: For simple cases like two-bone chains (arm or leg), we can use closed-form mathematical solutions - similar to how we directly interpolate between two keyframes +* *Cyclic Coordinate Descent (CCD)*: An iterative approach that adjusts one joint at a time, working backward from the end effector +* *FABRIK (Forward And Backward Reaching Inverse Kinematics)*: Works by iteratively adjusting the entire chain, often converging faster than CCD +* *Jacobian Inverse*: Uses matrix operations to find optimal joint adjustments for complex chains + +===== Implementing Two-Bone IK + +The simplest and most common IK scenario involves a two-bone chain (like an arm or leg). Here's an implementation of the analytical two-bone IK solution: + +[source,cpp] +---- +// Two-bone IK solver +bool solveTwoBoneIK( + Node* rootNode, // The root joint (e.g., shoulder or hip) + Node* midNode, // The middle joint (e.g., elbow or knee) + Node* endNode, // The end effector (e.g., hand or foot) + const glm::vec3& targetPosition, // Target world position + const glm::vec3& hingeAxis, // Axis of rotation for the middle joint + float preferredAngle = 0.0f // Preferred angle for resolving ambiguity +) { + // Get the original global positions + glm::mat4 rootGlobal = rootNode->getGlobalMatrix(); + glm::mat4 midGlobal = midNode->getGlobalMatrix(); + glm::mat4 endGlobal = endNode->getGlobalMatrix(); + + glm::vec3 rootPos = glm::vec3(rootGlobal[3]); + glm::vec3 midPos = glm::vec3(midGlobal[3]); + glm::vec3 endPos = glm::vec3(endGlobal[3]); + + // Calculate bone lengths + float bone1Length = glm::length(midPos - rootPos); + float bone2Length = glm::length(endPos - midPos); + float totalLength = bone1Length + bone2Length; + + // Calculate the distance to the target + float targetDistance = glm::length(targetPosition - rootPos); + + // Check if the target is reachable + if (targetDistance > totalLength) { + // Target is too far - stretch as far as possible + glm::vec3 direction = glm::normalize(targetPosition - rootPos); + + // Set mid node position + glm::vec3 newMidPos = rootPos + direction * bone1Length; + + // Convert to local space and update node + glm::mat4 rootInv = glm::inverse(rootGlobal); + glm::vec3 localMidPos = glm::vec3(rootInv * glm::vec4(newMidPos, 1.0f)); + midNode->translation = localMidPos; + + // Update mid global matrix after changes + midGlobal = midNode->getGlobalMatrix(); + + // Set end node position + glm::vec3 newEndPos = newMidPos + direction * bone2Length; + + // Convert to local space and update node + glm::mat4 midInv = glm::inverse(midGlobal); + glm::vec3 localEndPos = glm::vec3(midInv * glm::vec4(newEndPos, 1.0f)); + endNode->translation = localEndPos; + + return false; // Target not fully reached + } + + // Target is reachable - apply cosine law to find the angles + float a = bone1Length; + float b = targetDistance; + float c = bone2Length; + + // Calculate the angle between the first bone and the target direction + float cosAngle1 = (b*b + a*a - c*c) / (2*b*a); + cosAngle1 = glm::clamp(cosAngle1, -1.0f, 1.0f); // Avoid numerical errors + float angle1 = acos(cosAngle1); + + // Calculate the direction to the target + glm::vec3 targetDir = glm::normalize(targetPosition - rootPos); + + // Create a rotation that aligns the x-axis with the target direction + glm::vec3 xAxis(1.0f, 0.0f, 0.0f); + glm::vec3 rotAxis = glm::cross(xAxis, targetDir); + + if (glm::length(rotAxis) < 0.001f) { + // Target is along the x-axis, use the up vector + rotAxis = glm::vec3(0.0f, 1.0f, 0.0f); + } else { + rotAxis = glm::normalize(rotAxis); + } + + float rotAngle = acos(glm::dot(xAxis, targetDir)); + glm::quat targetRot = glm::angleAxis(rotAngle, rotAxis); + + // Create a rotation around the target direction by the preferred angle + glm::quat prefRot = glm::angleAxis(preferredAngle, targetDir); + + // Combine rotations + glm::quat finalRot = prefRot * targetRot * glm::angleAxis(angle1, hingeAxis); + + // Apply the rotation to the root node + rootNode->rotation = finalRot; + + // Update the mid node's global matrix after root changes + midGlobal = midNode->getGlobalMatrix(); + midPos = glm::vec3(midGlobal[3]); + + // Calculate the angle for the middle joint + float cosAngle2 = (a*a + c*c - b*b) / (2*a*c); + cosAngle2 = glm::clamp(cosAngle2, -1.0f, 1.0f); // Avoid numerical errors + float angle2 = acos(cosAngle2); + + // The middle joint bends in the opposite direction (PI - angle2) + glm::quat midRot = glm::angleAxis(glm::pi() - angle2, hingeAxis); + midNode->rotation = midRot; + + return true; // Target reached +} +---- + +This implementation: + +1. Calculates the positions and lengths of the bones +2. Checks if the target is reachable +3. Uses the law of cosines to calculate the necessary angles +4. Applies rotations to the joints to reach the target position + +===== Implementing CCD (Cyclic Coordinate Descent) + +For chains with more than two bones, CCD is a popular iterative approach: + +[source,cpp] +---- +// CCD IK solver +void solveCCDIK( + std::vector chain, // Joint chain from root to end effector + const glm::vec3& targetPosition, // Target world position + int maxIterations = 10, // Maximum iterations + float threshold = 0.01f // Distance threshold for success +) { + if (chain.size() < 2) return; + + // Get the end effector + Node* endEffector = chain.back(); + + for (int iteration = 0; iteration < maxIterations; iteration++) { + // Get current end effector position + glm::vec3 endPos = glm::vec3(endEffector->getGlobalMatrix()[3]); + + // Check if we're close enough to the target + if (glm::distance(endPos, targetPosition) < threshold) { + return; // Success + } + + // Work backwards from the second-to-last joint to the root + for (int i = chain.size() - 2; i >= 0; i--) { + Node* currentJoint = chain[i]; + + // Get joint position in world space + glm::mat4 jointGlobal = currentJoint->getGlobalMatrix(); + glm::vec3 jointPos = glm::vec3(jointGlobal[3]); + + // Get updated end effector position + endPos = glm::vec3(endEffector->getGlobalMatrix()[3]); + + // Calculate vectors from joint to end effector and target + glm::vec3 toEnd = glm::normalize(endPos - jointPos); + glm::vec3 toTarget = glm::normalize(targetPosition - jointPos); + + // Calculate rotation to align the vectors + float cosAngle = glm::dot(toEnd, toTarget); + cosAngle = glm::clamp(cosAngle, -1.0f, 1.0f); + + float angle = acos(cosAngle); + + // If the angle is small enough, skip this joint + if (angle < 0.01f) continue; + + // Calculate rotation axis + glm::vec3 rotAxis = glm::cross(toEnd, toTarget); + + // Handle the case where vectors are parallel + if (glm::length(rotAxis) < 0.001f) { + // Find an arbitrary perpendicular axis + glm::vec3 tempAxis(0.0f, 1.0f, 0.0f); + if (abs(glm::dot(toEnd, tempAxis)) > 0.9f) { + tempAxis = glm::vec3(1.0f, 0.0f, 0.0f); + } + rotAxis = glm::cross(toEnd, tempAxis); + } + + rotAxis = glm::normalize(rotAxis); + + // Create rotation quaternion + glm::quat rotation = glm::angleAxis(angle, rotAxis); + + // Apply rotation to the joint + currentJoint->rotation = rotation * currentJoint->rotation; + + // Check if we're close enough after this adjustment + endPos = glm::vec3(endEffector->getGlobalMatrix()[3]); + if (glm::distance(endPos, targetPosition) < threshold) { + return; // Success + } + } + } +} +---- + +This CCD implementation: + +1. Iteratively processes each joint from the end effector toward the root +2. For each joint, calculates the rotation needed to bring the end effector closer to the target +3. Applies the rotation and continues to the next joint +4. Repeats until the target is reached or the maximum iterations are exhausted + +===== Implementing FABRIK (Forward And Backward Reaching IK) + +FABRIK is another popular IK algorithm that often converges faster than CCD: + +[source,cpp] +---- +// FABRIK IK solver +void solveFABRIK( + std::vector chain, // Joint chain from root to end effector + const glm::vec3& targetPosition, // Target world position + bool constrainRoot = true, // Whether to keep the root fixed + int maxIterations = 10, // Maximum iterations + float threshold = 0.01f // Distance threshold for success +) { + if (chain.size() < 2) return; + + // Store original positions and bone lengths + std::vector positions; + std::vector lengths; + glm::vec3 rootOriginalPos; + + // Initialize positions and calculate lengths + for (size_t i = 0; i < chain.size(); i++) { + glm::vec3 pos = glm::vec3(chain[i]->getGlobalMatrix()[3]); + positions.push_back(pos); + + if (i > 0) { + lengths.push_back(glm::distance(positions[i], positions[i-1])); + } + } + + rootOriginalPos = positions[0]; + + // Check if the target is reachable + float totalLength = 0.0f; + for (float length : lengths) { + totalLength += length; + } + + glm::vec3 rootToTarget = targetPosition - positions[0]; + float targetDistance = glm::length(rootToTarget); + + if (targetDistance > totalLength) { + // Target is unreachable - stretch the chain + glm::vec3 direction = glm::normalize(rootToTarget); + + // Set all joints along the line to the target + positions[0] = constrainRoot ? rootOriginalPos : positions[0]; + + for (size_t i = 1; i < chain.size(); i++) { + positions[i] = positions[i-1] + direction * lengths[i-1]; + } + } else { + // Target is reachable - apply FABRIK + for (int iteration = 0; iteration < maxIterations; iteration++) { + // Check if we're already close enough + if (glm::distance(positions.back(), targetPosition) < threshold) { + break; + } + + // BACKWARD PASS: Set the end effector to the target and work backwards + positions.back() = targetPosition; + + for (int i = chain.size() - 2; i >= 0; i--) { + // Get the direction from this joint to the next + glm::vec3 direction = glm::normalize(positions[i] - positions[i+1]); + + // Set the position of this joint + positions[i] = positions[i+1] + direction * lengths[i]; + } + + // FORWARD PASS: Fix the root and work forwards + if (constrainRoot) { + positions[0] = rootOriginalPos; + } + + for (size_t i = 0; i < chain.size() - 1; i++) { + // Get the direction from this joint to the next + glm::vec3 direction = glm::normalize(positions[i+1] - positions[i]); + + // Set the position of the next joint + positions[i+1] = positions[i] + direction * lengths[i]; + } + + // Check if we're close enough after this iteration + if (glm::distance(positions.back(), targetPosition) < threshold) { + break; + } + } + } + + // Apply the new positions to the joints by calculating rotations + for (size_t i = 0; i < chain.size() - 1; i++) { + Node* currentJoint = chain[i]; + + // Calculate the original direction in local space + glm::mat4 parentGlobal = i > 0 ? chain[i-1]->getGlobalMatrix() : glm::mat4(1.0f); + glm::mat4 localToGlobal = currentJoint->getGlobalMatrix() * glm::inverse(parentGlobal); + glm::vec3 originalDir = glm::normalize(glm::vec3(localToGlobal * glm::vec4(1.0f, 0.0f, 0.0f, 0.0f))); + + // Calculate the new direction + glm::vec3 newDir = glm::normalize(positions[i+1] - positions[i]); + + // Calculate the rotation to align the directions + float cosAngle = glm::dot(originalDir, newDir); + cosAngle = glm::clamp(cosAngle, -1.0f, 1.0f); + + float angle = acos(cosAngle); + + // If the angle is small, skip this joint + if (angle < 0.01f) continue; + + // Calculate rotation axis + glm::vec3 rotAxis = glm::cross(originalDir, newDir); + + // Handle the case where vectors are parallel + if (glm::length(rotAxis) < 0.001f) { + // Find an arbitrary perpendicular axis + glm::vec3 tempAxis(0.0f, 1.0f, 0.0f); + if (abs(glm::dot(originalDir, tempAxis)) > 0.9f) { + tempAxis = glm::vec3(1.0f, 0.0f, 0.0f); + } + rotAxis = glm::cross(originalDir, tempAxis); + } + + rotAxis = glm::normalize(rotAxis); + + // Create rotation quaternion + glm::quat rotation = glm::angleAxis(angle, rotAxis); + + // Apply rotation to the joint + currentJoint->rotation = rotation * currentJoint->rotation; + } +} +---- + +The FABRIK algorithm: + +1. Works by alternating between forward and backward passes along the joint chain +2. In the backward pass, it positions joints working from the end effector toward the root +3. In the forward pass, it repositions joints from the root toward the end effector +4. This process quickly converges to a solution that satisfies the constraints + +===== IK Constraints + +In practice, IK systems need constraints to produce realistic results: + +[source,cpp] +---- +// Apply joint constraints to a node +void applyJointConstraints(Node* node, + const glm::vec3& minAngles, + const glm::vec3& maxAngles) { + // Convert quaternion to Euler angles + glm::vec3 eulerAngles = glm::degrees(glm::eulerAngles(node->rotation)); + + // Apply constraints + eulerAngles.x = glm::clamp(eulerAngles.x, minAngles.x, maxAngles.x); + eulerAngles.y = glm::clamp(eulerAngles.y, minAngles.y, maxAngles.y); + eulerAngles.z = glm::clamp(eulerAngles.z, minAngles.z, maxAngles.z); + + // Convert back to quaternion + glm::quat constrainedRotation = glm::quat(glm::radians(eulerAngles)); + + // Apply the constrained rotation + node->rotation = constrainedRotation; +} +---- + +===== Integrating IK with Animation + +Now that we've implemented several IK algorithms, let's see how they integrate with our animation system. Remember that our animation system updates node transformations based on keyframes, but sometimes we need to override or adjust these transformations based on runtime conditions. Here's how we can blend IK adjustments with our existing animation playback: + +[source,cpp] +---- +// Apply IK on top of an animation +void applyIKToAnimation(Model* model, uint32_t animationIndex, float deltaTime, + Node* endEffector, const glm::vec3& targetPosition, + float ikWeight = 1.0f) { + // First, update the animation normally + model->updateAnimation(animationIndex, deltaTime); + + // If IK weight is zero, we're done + if (ikWeight <= 0.0f) return; + + // Build the joint chain from end effector to root + std::vector chain; + Node* current = endEffector; + + // Add up to 3 joints to the chain (e.g., hand, elbow, shoulder) + while (current && chain.size() < 3) { + chain.push_back(current); + current = current->parent; + } + + // Reverse the chain to go from root to end effector + std::reverse(chain.begin(), chain.end()); + + // Store original rotations + std::vector originalRotations; + for (Node* node : chain) { + originalRotations.push_back(node->rotation); + } + + // Apply IK + solveTwoBoneIK(chain[0], chain[1], chain[2], targetPosition, + glm::vec3(0.0f, 0.0f, 1.0f)); + + // Blend between original and IK rotations based on weight + if (ikWeight < 1.0f) { + for (size_t i = 0; i < chain.size(); i++) { + chain[i]->rotation = glm::slerp(originalRotations[i], + chain[i]->rotation, + ikWeight); + } + } +} +---- + +===== Use Cases and Limitations + +IK is powerful but comes with considerations: + +* *Performance*: Iterative IK algorithms can be computationally expensive +* *Stability*: IK can produce jittery results without proper damping and constraints +* *Realism*: Without constraints, IK can produce physically impossible poses +* *Integration*: Blending IK with existing animations requires careful tuning + +Despite these challenges, IK is essential for: + +* *Environmental Adaptation*: Making characters interact with varying terrain and objects +* *Procedural Animation*: Generating animations that respond to dynamic conditions +* *Interactive Gameplay*: Allowing precise control over character limbs for gameplay mechanics + +==== Animation State Machines + +So far, we've explored how to play individual animations, blend between animations, and adjust animations with IK. But in a real game, characters often have dozens of animations that need to be triggered based on player input and game state. How do we organize and manage all these animations and their transitions? This is where animation state machines come in. + +For complex characters, a state machine can manage transitions between animations: + +[source,cpp] +---- +enum class AnimationState { + IDLE, + WALKING, + RUNNING, + JUMPING +}; + +class CharacterAnimator { +private: + Model* model; + AnimationState currentState = AnimationState::IDLE; + float blendTime = 0.3f; + float currentBlend = 0.0f; + + struct StateAnimation { + uint32_t animationIndex; + float speed; + bool loop; + }; + + std::unordered_map stateMap; + +public: + CharacterAnimator(Model* model) : model(model) { + // Map states to animations + stateMap[AnimationState::IDLE] = {0, 1.0f, true}; + stateMap[AnimationState::WALKING] = {1, 1.0f, true}; + stateMap[AnimationState::RUNNING] = {2, 1.0f, true}; + stateMap[AnimationState::JUMPING] = {3, 1.0f, false}; + } + + void setState(AnimationState newState) { + if (newState != currentState) { + // Start blending to new animation + currentBlend = 0.0f; + currentState = newState; + } + } + + void update(float deltaTime) { + // Handle blending if needed + if (currentBlend < blendTime) { + currentBlend += deltaTime; + float t = currentBlend / blendTime; + // Implement blending logic here + } else { + // Just update current animation + StateAnimation& anim = stateMap[currentState]; + model->updateAnimation(anim.animationIndex, deltaTime * anim.speed); + } + } +}; +---- + +==== Procedural Animations + +You can also create animations procedurally: + +[source,cpp] +---- +void applyProceduralAnimation(float time) { + // Find the head node + Node* headNode = nullptr; + for (auto node : model.linearNodes) { + if (node->name == "Head") { + headNode = node; + break; + } + } + + if (headNode) { + // Apply a simple bobbing motion + float bobAmount = sin(time * 2.0f) * 0.05f; + headNode->translation.y += bobAmount; + + // Apply a simple looking around motion + float lookAmount = sin(time * 0.5f) * 0.2f; + glm::quat lookRotation = glm::angleAxis(lookAmount, glm::vec3(0.0f, 1.0f, 0.0f)); + headNode->rotation = lookRotation * headNode->rotation; + } +} +---- + +=== Performance Considerations + +Animations can be computationally expensive, especially with complex models. Here are some optimization techniques: + +* *Level of Detail (LOD)*: Use simpler animations for distant objects +* *Animation Culling*: Don't update animations for objects outside the view frustum +* *Keyframe Reduction*: Reduce the number of keyframes in animations that don't need high precision +* *Parallel Processing*: Update animations in parallel using multiple threads + +=== Conclusion + +Our animation system provides a solid foundation for bringing 3D models to life. By leveraging the glTF format and our scene graph structure, we can efficiently load, play, and blend animations to create dynamic and engaging scenes. + +In the next chapter, we'll wrap up our exploration of the model loading system and discuss future enhancements. + +link:07_scene_rendering.adoc[Previous: Rendering the Scene] | link:09_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc b/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc new file mode 100644 index 00000000..54e45925 --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/09_conclusion.adoc @@ -0,0 +1,28 @@ +:pp: {plus}{plus} + += Loading Models: Conclusion + +== Conclusion + +In this chapter, we've completed our simple engine by integrating model loading capabilities with the architecture and camera systems developed in the previous chapters. Building upon our knowledge of glTF from the link:../../15_GLTF_KTX2_Migration.html[main tutorial], we've implemented: + +1. A hierarchical scene graph for organizing 3D objects +2. Support for glTF animations +3. A PBR material system that leverages glTF's material properties +4. Multi-object rendering with individual transformations + +This approach demonstrates how the concepts learned throughout this tutorial series can be structured into a more reusable and extensible engine architecture. By combining the engine architecture principles, camera transformation systems, and now model loading capabilities, we've created a foundation that you can build upon for your own projects. + +As you continue to develop your engine, consider exploring these advanced topics: + +1. A more sophisticated material system +2. Advanced lighting techniques +3. Post-processing effects +4. Physics integration +5. Audio systems + +The code for this chapter can be found in the `simple_engine/20_loading_models.cpp` file. + +link:../../attachments/simple_engine/20_loading_models.cpp[C{pp} code] + +link:08_animations.adoc[Previous: Updating Animations] | link:../Subsystems/01_introduction.adoc[Next: Subsystems] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Loading_Models/index.adoc b/en/Building_a_Simple_Engine/Loading_Models/index.adoc new file mode 100644 index 00000000..7e8bf09e --- /dev/null +++ b/en/Building_a_Simple_Engine/Loading_Models/index.adoc @@ -0,0 +1,23 @@ +:pp: {plus}{plus} + += Loading Models: Integrating a glTF loader with animation and PBR + +== Chapter Overview + +Welcome to the third chapter of the "Building a Simple Engine" series! After exploring engine architecture and camera systems in the previous chapters, we'll now focus on creating a robust model loading system that can handle modern 3D assets using the glTF format. We'll implement a scene graph, animation system, and PBR rendering to complete our engine foundation. + +This chapter is divided into several sections to make it easier to follow: + +1. link:01_introduction.adoc[Introduction] - An overview of what we'll be building and prerequisites +2. link:02_project_setup.adoc[Setting Up the Project] - How to structure our engine project +3. link:03_model_system.adoc[Implementing the Model Loading System] - Creating the core data structures +4. link:04_loading_gltf.adoc[Loading a glTF Model] - Parsing and processing glTF files +5. link:05_pbr_rendering.adoc[Implementing PBR Rendering] - Setting up physically-based rendering +6. link:06_multiple_objects.adoc[Rendering Multiple Objects] - Managing multiple model instances +7. link:07_scene_rendering.adoc[Rendering the Scene] - Drawing the scene graph +8. link:08_animations.adoc[Updating Animations] - Animating models +9. link:09_conclusion.adoc[Conclusion] - Summary and future directions + +Each section builds upon the previous ones, so it's recommended to follow them in order. This chapter also builds upon the concepts introduced in the Engine Architecture and Camera Transformations chapters. By completing this chapter, you'll have a comprehensive foundation for a Vulkan-based 3D engine with a well-structured architecture, camera system, and model loading capabilities. + +link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc new file mode 100644 index 00000000..ad79de56 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/01_introduction.adoc @@ -0,0 +1,40 @@ +:pp: {plus}{plus} + += Mobile Development: Introduction + +== Introduction to Mobile Development + +In previous chapters, we've built a solid foundation for our simple engine, implementing core components like the rendering pipeline, camera systems, model loading, essential subsystems, and tooling. Now, we're ready to explore how to adapt our engine for mobile platforms, specifically Android and iOS. + +Mobile development presents unique challenges and opportunities for Vulkan applications. The constraints of mobile hardware—limited power, memory, and thermal capacity—require careful optimization and consideration of platform-specific features. At the same time, mobile platforms offer exciting possibilities for reaching a wider audience with your applications. + +=== What We'll Cover + +This chapter will guide you through the complex landscape of mobile Vulkan development, where desktop assumptions often don't apply. We'll start by examining the platform-specific requirements of Android and iOS, which present unique challenges in setup, lifecycle management, and input handling. Mobile applications face constraints that desktop applications rarely encounter—sudden interruptions, battery concerns, and varying hardware capabilities all require careful consideration in your engine design. + +Performance optimization takes on critical importance in mobile environments where every watt of power consumption and every millisecond of frame time affects user experience. We'll explore essential techniques like efficient texture formats, along with mobile-specific optimizations that can mean the difference between smooth performance and user frustration. + +Understanding the fundamental architectural differences between mobile and desktop GPUs becomes essential for effective optimization. We'll compare Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR) approaches, helping you understand why techniques that work well on desktop might perform poorly on mobile, and how to design rendering strategies that leverage mobile GPU strengths. + +Finally, we'll explore the Vulkan extensions specifically designed for mobile platforms. Extensions like VK_KHR_dynamic_rendering_local_read, VK_KHR_dynamic_rendering, and VK_EXT_shader_tile_image unlock performance opportunities that can dramatically improve your application's efficiency on mobile hardware, transforming acceptable performance into exceptional user experiences. + +=== Prerequisites + +This chapter represents the culmination of everything we've built throughout the previous chapters, as mobile development requires deep integration with all engine systems. You'll need solid mastery of Vulkan fundamentals and the engine architecture we've developed, since mobile optimization often requires fine-tuning at every level—from resource management and rendering pipelines to memory allocation and synchronization. + +Modern C++ expertise becomes particularly valuable in mobile development, where performance constraints demand efficient code and careful resource management. C++17 and C++20 features like constexpr, structured bindings, and concepts help create mobile-optimized code that performs well under strict power and thermal limitations. + +Understanding basic mobile development concepts will provide crucial context for the platform-specific decisions we'll make. Mobile applications operate under constraints that desktop applications rarely face—app lifecycle events, varying screen densities, touch input paradigms, and the need to preserve battery life all influence how we design and implement our Vulkan engine for mobile platforms. + +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* xref:../../11_Compute_Shader.adoc[Compute shaders] + +Let's begin by exploring the platform considerations for Android and iOS. + +link:../Tooling/07_conclusion.adoc[Previous: Tooling Conclusion] | link:02_platform_considerations.adoc[Next: Platform Considerations for Android and iOS] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc new file mode 100644 index 00000000..467eb51b --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/02_platform_considerations.adoc @@ -0,0 +1,197 @@ +:pp: {plus}{plus} + += Mobile Development: Platform Considerations + +== Platform Considerations for Android and iOS + +Developing Vulkan applications for mobile platforms requires understanding the specific requirements and constraints of Android and iOS. In this section, we'll explore the key considerations for each platform and how to adapt your engine accordingly. + +=== Android Platform Considerations + +Android has supported Vulkan since version 7.0 (Nougat), but the level of support varies across devices. Here are the key considerations for Android development: + +==== Setting Up Vulkan on Android + +To use Vulkan on Android, you need to: + +* *Declare Vulkan Support*: In your AndroidManifest.xml, declare that your +application uses Vulkan: + +[source,xml] +---- + + + + + + + +---- + +* *Initialize Vulkan*: Use the Android Native Development Kit (NDK) to +initialize Vulkan. The process is similar to desktop Vulkan, but you'll need + to obtain the native window handle from Android: + +[source,cpp] +---- +// Get the native window handle from Android +ANativeWindow* native_window = ANativeWindow_fromSurface(env, surface); + +// Create the Vulkan surface +VkAndroidSurfaceCreateInfoKHR create_info{}; +create_info.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR; +create_info.window = native_window; + +VkSurfaceKHR vulkan_surface; +vkCreateAndroidSurfaceKHR(instance, &create_info, nullptr, &vulkan_surface); +---- + +==== Android Lifecycle Management + +Android applications have a complex lifecycle that you need to handle properly: + +1. *Activity Pausing and Resuming*: When your application is paused (e.g., when the user switches to another app), you should release Vulkan resources and recreate them when the application resumes. + +2. *Surface Changes*: The surface can change due to configuration changes (e.g., rotation). You need to handle these changes by recreating the swapchain. + +3. *Memory Pressure*: Android can reclaim memory from your application at any time. Design your engine to handle memory pressure gracefully. + +==== Android Input Handling + +Android input handling differs from desktop: + +1. *Touch Input*: Instead of mouse input, you'll need to handle touch events, including multi-touch gestures. + +2. *Sensors*: Android devices have various sensors (accelerometer, gyroscope, etc.) that you can use for input. + +==== Vendor-Specific Considerations + +Different Android device manufacturers may have specific considerations: + +1. *Custom Android Versions*: Many manufacturers use customized versions of Android. Test your application on various devices to ensure compatibility. + +2. *GPU Architectures*: Different vendors use different GPU architectures (Adreno, Mali, PowerVR, etc.). Each has unique performance characteristics. + +3. *Alternative App Stores*: Some devices may not have Google Play Services. Consider distributing through alternative app stores when necessary. + +4. *SoC Variations*: System-on-Chip variations affect performance. Most mobile GPUs are tile-based renderers, so optimize your rendering pipeline accordingly using the techniques described in the TBR section. + +[source,cpp] +---- +// Example of checking for specific device vendors +bool check_device_vendor(vk::PhysicalDevice physical_device, uint32_t vendor_id) { + vk::PhysicalDeviceProperties props = physical_device.getProperties(); + return props.vendorID == vendor_id; +} + +// Common vendor IDs +const uint32_t VENDOR_ID_QUALCOMM = 0x5143; // Adreno +const uint32_t VENDOR_ID_ARM = 0x13B5; // Mali +const uint32_t VENDOR_ID_IMAGINATION = 0x1010; // PowerVR +const uint32_t VENDOR_ID_HUAWEI = 0x19E5; // Kirin + +// You can then apply vendor-specific optimizations if needed +void configure_for_device(vk::PhysicalDevice physical_device) { + if (check_device_vendor(physical_device, VENDOR_ID_HUAWEI)) { + // Apply Huawei-specific optimizations if needed + } + // Handle other vendors as needed +} +---- + +=== iOS Platform Considerations + +iOS supports Vulkan through MoltenVK, a translation layer that maps Vulkan to Metal. Here are the key considerations for iOS development: + +==== Setting Up MoltenVK on iOS + +To use Vulkan on iOS, you need to: + +* *Include MoltenVK*: Add the MoltenVK framework to your Xcode project. + +* *Initialize MoltenVK*: Initialize MoltenVK before creating your Vulkan +instance: + +[source,cpp] +---- +// Initialize MoltenVK +MVKConfiguration config{}; +vkGetMoltenVKConfigurationMVK(nullptr, &config); +config.debugMode = true; // Enable debug mode during development +vkSetMoltenVKConfigurationMVK(nullptr, &config); + +// Create Vulkan instance as usual +// ... +---- + +* *Create a Metal-Compatible Surface*: Create a Vulkan surface from a +CAMetalLayer: + +[source,cpp] +---- +// Get the Metal layer from your UIView +CAMetalLayer* metal_layer = (CAMetalLayer*)layer; + +// Create the Vulkan surface +VkMetalSurfaceCreateInfoEXT create_info{}; +create_info.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT; +create_info.pLayer = metal_layer; + +VkSurfaceKHR vulkan_surface; +vkCreateMetalSurfaceEXT(instance, &create_info, nullptr, &vulkan_surface); +---- + +==== iOS Lifecycle Management + +iOS applications also have a lifecycle that you need to handle: + +1. *Application State Changes*: Handle applicationWillResignActive, applicationDidBecomeActive, etc., by releasing and recreating Vulkan resources as needed. + +2. *Memory Warnings*: iOS can send memory warnings when the system is low on memory. Handle these by releasing non-essential resources. + +==== iOS Input Handling + +iOS input handling is similar to Android but with some differences: + +1. *Touch Input*: iOS has its own touch event system that you'll need to integrate with your engine. + +2. *Sensors*: iOS devices also have various sensors that you can use for input. + +=== Cross-Platform Considerations + +To maintain a single codebase for both Android and iOS (and potentially desktop), consider: + +* *Abstraction Layers*: Create platform-specific abstraction layers for +window creation, input handling, and other platform-specific functionality. + +* *Conditional Compilation*: Use preprocessor directives to handle +platform-specific code: + +[source,cpp] +---- +#ifdef __ANDROID__ + // Android-specific code +#elif defined(__APPLE__) + // iOS-specific code +#else + // Desktop-specific code +#endif +---- + +* *Feature Detection*: Use Vulkan's feature detection mechanisms to adapt to +the capabilities of the device, rather than making assumptions based on the platform. + +=== Best Practices for Mobile Platform Integration + +1. *Test on Real Devices*: Emulators and simulators may not accurately represent the performance and behavior of real devices. + +2. *Handle Different Screen Sizes and Aspect Ratios*: Mobile devices come in various sizes and aspect ratios. Design your UI and rendering to adapt accordingly. + +3. *Consider Battery Life*: Mobile users are sensitive to battery drain. Optimize your engine to minimize power consumption. + +4. *Respect Platform Guidelines*: Follow the design and user experience guidelines for each platform to ensure your application feels native. + +In the next section, we'll explore performance optimizations specifically tailored for mobile hardware, focusing on texture formats and memory usage. + +link:01_introduction.adoc[Previous: Introduction] | link:03_performance_optimizations.adoc[Next: Performance Optimizations for Mobile] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc new file mode 100644 index 00000000..ed9dcdc1 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/03_performance_optimizations.adoc @@ -0,0 +1,260 @@ +:pp: {plus}{plus} + += Mobile Development: Performance Optimizations + +== Performance Optimizations for Mobile + +Mobile devices have significantly different hardware constraints compared to desktop systems. In this section, we'll explore key performance optimizations that are essential for achieving good performance on mobile platforms. + +[NOTE] +==== +This chapter covers general mobile performance. For practices that arise specifically because the GPU is tile-based (TBR), see link:04_rendering_approaches.adoc[Rendering Approaches: Tile-Based Rendering]. +==== + +=== Texture Optimizations + +[NOTE] +==== +We focus on mobile‑specific decisions here. For general Vulkan image creation, staging uploads, and descriptor setup, refer back to earlier chapters in the engine series—link:../Engine_Architecture/04_resource_management.adoc[Resource Management], link:../Engine_Architecture/05_rendering_pipeline.adoc[Rendering Pipeline]—or the Vulkan Guide (https://docs.vulkan.org/guide/latest/). +==== + +Textures are often the largest consumers of memory in a graphics application. Optimizing them is crucial for performance on both mobile and desktop. + +==== Efficient Texture Formats + +Choosing the right texture format is crucial across platforms; what differs is which formats are natively supported by a given device/driver: + +1. *Compressed Formats*: Use hardware-supported compressed formats whenever possible: + - *ASTC* (Adaptive Scalable Texture Compression): Widely supported on modern mobile GPUs and increasingly available on desktop; excellent quality-to-size ratio with flexible block sizes. + - *ETC2/EAC*: Required for OpenGL ES 3.0 and supported by most Android devices; commonly available on Vulkan stacks, too. + - *PVRTC*: Primarily supported on iOS devices with PowerVR GPUs. + - *BC* (Block Compression, a.k.a. DXT/BCn): Ubiquitous on desktop; supported by some mobile GPUs. + +2. *Format Selection Based on Content and Support*: Choose formats based on the type of texture and what the device reports: + - For high detail (normals, roughness): prefer ASTC 4x4 or 6x6 when supported; on desktop, BC5/BC7 are common alternatives. + - For albedo/basecolor: ASTC 6x6–8x8 works well when available; on desktop, BC1/BC7 are typical. + - For single-channel data: consider R8 or compressed single-channel alternatives when available. + +[NOTE] +==== +This guidance is not mobile-only: block compression reduces memory footprint and bandwidth on all platforms. The Mobile chapter highlights it because bandwidth and power are tighter constraints on phones/tablets. On desktop, the same benefits apply; the primary difference is which formats are commonly available (e.g., BC on desktop, ASTC/ETC2 on many mobile devices). +==== + +Here's how to check for and use compressed formats in Vulkan: + +[source,cpp] +---- +bool is_format_supported(vk::PhysicalDevice physical_device, vk::Format format, vk::ImageTiling tiling, + vk::FormatFeatureFlags features) { + vk::FormatProperties props = physical_device.getFormatProperties(format); + + if (tiling == vk::ImageTiling::eLinear) { + return (props.linearTilingFeatures & features) == features; + } else if (tiling == vk::ImageTiling::eOptimal) { + return (props.optimalTilingFeatures & features) == features; + } + + return false; +} + +vk::Format find_supported_format(vk::PhysicalDevice physical_device, + const std::vector& candidates, + vk::ImageTiling tiling, + vk::FormatFeatureFlags features) { + for (vk::Format format : candidates) { + if (is_format_supported(physical_device, format, tiling, features)) { + return format; + } + } + + throw std::runtime_error("Failed to find supported format"); +} +---- + +=== Memory Optimizations + +Memory is a precious resource on all platforms. It tends to be more performance‑critical on mobile due to tighter bandwidth, power, and thermal budgets. Here are some key optimizations: + +==== Minimize Memory Allocations + +1. *Pool Allocations*: Use memory pools to reduce the overhead of frequent allocations and deallocations. + +2. *Suballocate from Larger Blocks*: Instead of creating many small Vulkan memory allocations, allocate larger blocks and suballocate from them: + +[source,cpp] +---- +class VulkanMemoryPool { +public: + VulkanMemoryPool(vk::Device device, vk::PhysicalDevice physical_device, + vk::DeviceSize block_size, uint32_t memory_type_index) + : device(device), block_size(block_size), memory_type_index(memory_type_index) { + allocate_new_block(); + } + + ~VulkanMemoryPool() { + for (auto& block : memory_blocks) { + device.freeMemory(block.memory); + } + } + + struct Allocation { + vk::DeviceMemory memory; + vk::DeviceSize offset; + vk::DeviceSize size; + }; + + Allocation allocate(vk::DeviceSize size, vk::DeviceSize alignment) { + // Find a block with enough space + for (auto& block : memory_blocks) { + vk::DeviceSize aligned_offset = align(block.next_offset, alignment); + if (aligned_offset + size <= block_size) { + Allocation alloc; + alloc.memory = block.memory; + alloc.offset = aligned_offset; + alloc.size = size; + + block.next_offset = aligned_offset + size; + return alloc; + } + } + + // No block has enough space, allocate a new one + allocate_new_block(); + return allocate(size, alignment); // Try again with the new block + } + +private: + struct MemoryBlock { + vk::DeviceMemory memory; + vk::DeviceSize next_offset = 0; + }; + + void allocate_new_block() { + vk::MemoryAllocateInfo alloc_info; + alloc_info.setAllocationSize(block_size); + alloc_info.setMemoryTypeIndex(memory_type_index); + + MemoryBlock block; + block.memory = device.allocateMemory(alloc_info); + block.next_offset = 0; + + memory_blocks.push_back(block); + } + + vk::DeviceSize align(vk::DeviceSize offset, vk::DeviceSize alignment) { + return (offset + alignment - 1) & ~(alignment - 1); + } + + vk::Device device; + vk::DeviceSize block_size; + uint32_t memory_type_index; + std::vector memory_blocks; +}; +---- + +==== Reduce Bandwidth Usage + +1. *Minimize State Changes*: Group draw calls by material to reduce state changes. + +2. *Use Smaller Data Types*: Use 16-bit indices and half-precision floats where appropriate. + +3. *Optimize Vertex Formats*: Use packed vertex formats to reduce memory bandwidth: + +[source,cpp] +---- +// Traditional vertex format (48 bytes per vertex) +struct Vertex { + glm::vec3 position; // 12 bytes + glm::vec3 normal; // 12 bytes + glm::vec2 texCoord; // 8 bytes + glm::vec4 color; // 16 bytes +}; + +// Optimized vertex format (16 bytes per vertex) +struct OptimizedVertex { + // Position: 3 components, 16-bit float each + uint16_t position[3]; // 6 bytes + + // Normal: 2 components (can reconstruct Z), 8-bit signed normalized + int8_t normal[2]; // 2 bytes + + // TexCoord: 2 components, 16-bit float each + uint16_t texCoord[2]; // 4 bytes + + // Color: 4 components, 8-bit unsigned normalized + uint8_t color[4]; // 4 bytes +}; +---- + +[NOTE] +==== +If you are targeting tile-based GPUs (TBR), bandwidth can be heavily impacted by attachment load/store behavior and tile flushes. See link:04_rendering_approaches.adoc[Rendering Approaches] — sections “Attachment Load/Store Operations on Tilers” and “Pipelining on Tilers: Subpass Dependencies and BY_REGION” for concrete guidance. +==== + +=== Draw Call Optimizations + +Mobile GPUs are particularly sensitive to draw call overhead: + +1. *Instancing*: Use instancing to reduce draw calls for repeated objects. + +2. *Batching*: Combine multiple objects into a single mesh where possible. + +3. *Level of Detail (LOD)*: Implement LOD systems to reduce geometry complexity for distant objects. + +[NOTE] +==== +On tile-based GPUs, reducing CPU overhead is important, but keeping work and data on-chip via proper pipelining and subpasses often yields larger gains. See link:04_rendering_approaches.adoc[Rendering Approaches] — “Pipelining on Tilers: Subpass Dependencies and BY_REGION” for barrier/subpass patterns, and “Attachment Load/Store Operations on Tilers” for loadOp/storeOp guidance that avoids external memory traffic. +==== + +=== Vendor-Specific Optimizations + +Different mobile GPU vendors have specific architectures that may benefit from targeted optimizations. + +==== Vendor-Specific GPU Optimizations + +Different mobile GPU vendors have specific architectures that benefit from targeted optimizations: + +* *Memory Management*: Many mobile SoCs have unified memory architecture: + - Use `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT` memory when possible + - Take advantage of fast CPU-GPU memory transfers in unified memory architectures + +* *Texture Compression*: Different devices support different texture +compression formats: + +[source,cpp] +---- +// Check for texture compression format support +bool supports_texture_format(vk::PhysicalDevice physical_device, vk::Format format) { + vk::FormatProperties props = physical_device.getFormatProperties(format); + return (props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage); +} + +// Get optimal texture format based on device capabilities +vk::Format get_optimal_texture_format(vk::PhysicalDevice physical_device) { + vk::PhysicalDeviceProperties props = physical_device.getProperties(); + vk::PhysicalDeviceFeatures features = physical_device.getFeatures(); + + // Check for ASTC support (widely supported on modern mobile GPUs) + // Most games are written with knowledge of what the assets were compressed with so it's standard practice to only ensure the required format is supported. + if (features.textureCompressionASTC_LDR) { + return vk::Format::eAstc8x8SrgbBlock; + } +} +---- + +* *Performance Monitoring*: Most vendors provide performance monitoring tools + that can help identify bottlenecks specific to their hardware. + +=== Best Practices for Mobile Performance + +1. *Profile on Target Devices*: Performance characteristics vary widely across mobile devices. Test on a range of hardware from different manufacturers and with different GPU architectures. + +2. *Monitor Temperature*: Mobile devices throttle performance when they get hot. Design your engine to adapt to thermal throttling. + +3. *Balance Quality and Performance*: Provide graphics settings that allow users to balance quality and performance based on their device capabilities. + +4. *Implement Adaptive Resolution*: Dynamically adjust rendering resolution based on performance metrics. + +In the next section, we'll explore different rendering approaches for mobile GPUs, focusing on the differences between Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR). + +link:02_platform_considerations.adoc[Previous: Platform Considerations] | link:04_rendering_approaches.adoc[Next: Rendering Approaches] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc new file mode 100644 index 00000000..e1a5db0c --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/04_rendering_approaches.adoc @@ -0,0 +1,381 @@ +:pp: {plus}{plus} + += Mobile Development: Rendering Approaches + +== Rendering Approaches for Mobile GPUs + +Mobile GPUs typically use different rendering architectures compared to desktop GPUs. Understanding these differences is crucial for optimizing your Vulkan application for mobile platforms. In this section, we'll explore the two main rendering approaches: Tile-Based Rendering (TBR) and Immediate Mode Rendering (IMR). + +=== Tile-Based Rendering (TBR) + +Most modern mobile GPUs use a tile-based rendering architecture, also known as Tile-Based Deferred Rendering (TBDR) in some implementations. + +==== How TBR Works + +1. *Tiling Phase*: The screen is divided into small tiles (typically 16x16 or 32x32 pixels). + +2. *Binning Phase*: The GPU determines which primitives (triangles) affect each tile. + +3. *Rendering Phase*: For each tile: + a. Load the primitives affecting that tile into on-chip memory. + b. Render the primitives to the tile. + c. Write the completed tile back to main memory. + +==== Advantages of TBR + +1. *Reduced Memory Bandwidth*: Since rendering happens in on-chip memory, there's less traffic to main memory. + +2. *Power Efficiency*: Lower memory bandwidth means lower power consumption, which is crucial for battery-powered devices. + +3. *Hidden Surface Removal*: Many TBR GPUs perform early depth testing during the binning phase, reducing overdraw. + +==== Optimizing for TBR + +To get the best performance from TBR GPUs, consider these optimizations: + +* *Transient Attachments*: Use transient attachments for render targets that are only used within a render pass: + +[source,cpp] +---- +vk::AttachmentDescription depth_attachment{}; +depth_attachment.setFormat(depth_format); +depth_attachment.setSamples(vk::SampleCountFlagBits::e1); +depth_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +depth_attachment.setStoreOp(vk::AttachmentStoreOp::eDontCare); // Don't store the result +depth_attachment.setStencilLoadOp(vk::AttachmentLoadOp::eDontCare); +depth_attachment.setStencilStoreOp(vk::AttachmentStoreOp::eDontCare); +depth_attachment.setInitialLayout(vk::ImageLayout::eUndefined); +depth_attachment.setFinalLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal); + +// When creating the image, mark the attachment as transient +vk::ImageCreateInfo image_info{}; +image_info.setImageType(vk::ImageType::e2D); +image_info.setExtent(vk::Extent3D(width, height, 1)); +image_info.setMipLevels(1); +image_info.setArrayLayers(1); +image_info.setFormat(depth_format); +image_info.setTiling(vk::ImageTiling::eOptimal); +image_info.setInitialLayout(vk::ImageLayout::eUndefined); +image_info.setUsage(vk::ImageUsageFlagBits::eDepthStencilAttachment | vk::ImageUsageFlagBits::eTransientAttachment); +image_info.setSamples(vk::SampleCountFlagBits::e1); +// Prefer lazily allocated memory for transient attachments when supported +// Choose memory with vk::MemoryPropertyFlagBits::eLazilyAllocated +---- + +* *Render Pass Structure*: Design your render passes to take advantage of +tile-based rendering: + - Use subpasses to keep rendering operations within the tile memory. + - Use the right load/store operations to minimize memory traffic. + +[source,cpp] +---- +// Create a render pass with multiple subpasses +vk::SubpassDescription subpass1{}; +subpass1.setPipelineBindPoint(vk::PipelineBindPoint::eGraphics); +subpass1.setColorAttachments(color_attachment_refs); +subpass1.setDepthStencilAttachment(&depth_attachment_ref); + +vk::SubpassDescription subpass2{}; +subpass2.setPipelineBindPoint(vk::PipelineBindPoint::eGraphics); +subpass2.setInputAttachments(input_attachment_refs); // Use output from subpass1 as input +subpass2.setColorAttachments(final_color_attachment_refs); + +// Create a dependency to ensure proper ordering +vk::SubpassDependency dependency{}; +dependency.setSrcSubpass(0); +dependency.setDstSubpass(1); +dependency.setSrcStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); +dependency.setDstStageMask(vk::PipelineStageFlagBits::eFragmentShader); +dependency.setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); +dependency.setDstAccessMask(vk::AccessFlagBits::eInputAttachmentRead); + +// Create the render pass +vk::RenderPassCreateInfo render_pass_info{}; +render_pass_info.setAttachments(attachments); +render_pass_info.setSubpasses({subpass1, subpass2}); +render_pass_info.setDependencies(dependency); + +vk::RenderPass render_pass = device.createRenderPass(render_pass_info); +---- + +==== Best Practices for TBR + +* *Avoid External Framebuffer Reads*: Avoid reading from images that require the tile to be flushed to external memory and reloaded; this is expensive on TBR. + - Local, same-pixel reads from on-chip/tile memory are fine and encouraged on tile-based GPUs. + - In Vulkan, use input attachments within subpasses or the `VK_KHR_dynamic_rendering_local_read` capability to perform tile-local reads without leaving tile memory. This is often referred to as pixel-local storage (PLS) on tile-based architectures. + +* *Optimize for Tile Size*: Consider the tile size when designing your rendering algorithm. For example, if you know the tile size is 16x16, you might organize your data or algorithms to work efficiently with that size. + +===== Attachment Load/Store Operations on Tilers + +On tile-based GPUs, correctly using loadOp and storeOp is one of the highest-impact optimizations: + +- Clear attachments with loadOp = CLEAR and initialLayout = UNDEFINED when you don't need previous contents. This avoids an external memory read for the tile. +- Use storeOp = DONT_CARE for attachments whose results are not needed after the render pass (e.g., transient depth or intermediate color targets). This can prevent flushing the tile back to main memory. +- For the swapchain image (or any image you will sample/transfer from later), use storeOp = STORE and set finalLayout appropriately (e.g., PRESENT_SRC_KHR for the swapchain). +- For MSAA, resolve within the same render pass so the hardware can resolve from tile memory and only store the resolved image to external memory. + +[source,cpp] +---- +// Color attachment that we clear and present +vk::AttachmentDescription color_attachment{}; +color_attachment.setFormat(swapchain_format); +color_attachment.setSamples(vk::SampleCountFlagBits::e1); +color_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +color_attachment.setStoreOp(vk::AttachmentStoreOp::eStore); // we need to present +color_attachment.setStencilLoadOp(vk::AttachmentLoadOp::eDontCare); +color_attachment.setStencilStoreOp(vk::AttachmentStoreOp::eDontCare); +color_attachment.setInitialLayout(vk::ImageLayout::eUndefined); // no need to load previous contents +color_attachment.setFinalLayout(vk::ImageLayout::ePresentSrcKHR); + +// Depth attachment used only within the pass +vk::AttachmentDescription depth_attachment{}; +depth_attachment.setFormat(depth_format); +depth_attachment.setSamples(vk::SampleCountFlagBits::e1); +depth_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +depth_attachment.setStoreOp(vk::AttachmentStoreOp::eDontCare); // don't flush depth to memory +depth_attachment.setStencilLoadOp(vk::AttachmentLoadOp::eDontCare); +depth_attachment.setStencilStoreOp(vk::AttachmentStoreOp::eDontCare); +depth_attachment.setInitialLayout(vk::ImageLayout::eUndefined); +depth_attachment.setFinalLayout(vk::ImageLayout::eDepthStencilAttachmentOptimal); +---- + +[NOTE] +==== +If you use dynamic rendering, the same rules apply via vk::RenderingAttachmentInfo loadOp/storeOp fields. +See Vulkan Guide for background: Render Passes and Subpasses, Tile-based GPUs. +==== + +===== Pipelining on Tilers: Subpass Dependencies and BY_REGION + +Tile-based GPUs benefit from fine-grained synchronization that keeps work and data on-chip: + +- Prefer subpasses with input attachments to keep producer/consumer within the same render pass, enabling tile-local reads. +- Use vk::DependencyFlagBits::eByRegion to scope hazards to the pixel regions actually written/read, avoiding unnecessary tile flushes. +- Avoid over-broad barriers (e.g., ALL_COMMANDS, MEMORY_READ/WRITE) that serialize the pipeline and may force external memory traffic. Use precise stage/access masks. + +Example: dependency from a color-writing subpass to a subpass that reads that color as an input attachment. + +[source,cpp] +---- +vk::SubpassDependency dep{}; +dep.setSrcSubpass(0); +dep.setDstSubpass(1); +dep.setSrcStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); +dep.setDstStageMask(vk::PipelineStageFlagBits::eFragmentShader); +dep.setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); +dep.setDstAccessMask(vk::AccessFlagBits::eInputAttachmentRead); +dep.setDependencyFlags(vk::DependencyFlagBits::eByRegion); +---- + +Example: external dependency to the first subpass of a render pass, allowing pipelining with prior pass while limiting scope by region. + +[source,cpp] +---- +vk::SubpassDependency externalDep{}; +externalDep.setSrcSubpass(VK_SUBPASS_EXTERNAL); +externalDep.setDstSubpass(0); +externalDep.setSrcStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput); +externalDep.setDstStageMask(vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eColorAttachmentOutput); +externalDep.setSrcAccessMask(vk::AccessFlagBits::eColorAttachmentWrite); +externalDep.setDstAccessMask(vk::AccessFlagBits::eDepthStencilAttachmentWrite | vk::AccessFlagBits::eColorAttachmentWrite); +externalDep.setDependencyFlags(vk::DependencyFlagBits::eByRegion); +---- + +[NOTE] +==== +With Synchronization2 (vkCmdPipelineBarrier2 and friends) avoid ALL_COMMANDS and prefer the minimal set of stages/access that capture your hazard. Use render pass/subpass structure when possible—it's the most tiler-friendly way to express pipelining. +==== + +For further guidance, see the xref:https://docs.vulkan.org/guide/latest/[Vulkan Guide] topics on Tile-based GPUs, Render Passes, and Synchronization. + +===== Memory Management + +To improve the efficiency of memory allocation in TBR architectures: + +* *Select Optimal Memory Types*: Choose the best matching memory type (with the appropriate VkMemoryPropertyFlags) when using vkAllocateMemory. + +* *Batch Allocations*: For each type of resource (e.g., index buffer, vertex buffer, and uniform buffer), allocate large chunks of memory with a specific size in one go when possible. + +* *Reuse Memory Resources*: Let multiple passes take turns using the allocated memory through time slicing. + +* *Use Cached Memory When Appropriate*: Consider using VK_MEMORY_PROPERTY_HOST_CACHED_BIT and manually flushing memory when it may be accessed by the CPU. This is often more efficient than VK_MEMORY_PROPERTY_HOST_COHERENT_BIT because the driver can refresh a large block of memory at once. + +* *Minimize Allocation Calls*: Avoid frequent calls to vkAllocateMemory. The number of memory allocations is limited by maxMemoryAllocationCount. + +===== Shader Optimizations + +Optimizing shaders for TBR architectures can significantly improve performance: + +* *Vectorized Memory Access*: Access memory in a vectorized manner to reduce access cycles and bandwidth. For example: + +[source,glsl] +---- +// Recommended: Vectorized access +struct TileStructSample { + vec4 data; +}; + +void main() { + uint idx = 0u; + TileStructSample ts[3]; + while (idx < 3u) { + ts[int(idx)].data = a; + idx++; + } +} + +// Not recommended: Non-vectorized access +struct TileStructSample { + float data1; + float data2; + float data3; + float data4; +}; + +void main() { + uint idx = 0u; + TileStructSample ts[3]; + while (idx < 3u) { + ts[int(idx)].data1 = a; + ts[int(idx)].data2 = b; + ts[int(idx)].data3 = c; + ts[int(idx)].data4 = d; + idx++; + } +} +---- + +* *Optimize Uniform Buffers*: Consider using push constants or macro constants instead of uniform buffers for small data. Avoid dynamic indexing when possible. + +* *Minimize Branching*: Reduce complex branch structures, branch nesting, and loop structures as they can harm parallelism. + +* *Use Half-Precision*: When appropriate, use half-precision floats to reduce bandwidth and power consumption. In SPIR-V, use relaxed-precision decoration on variables or results. + +===== Depth Testing Optimizations + +Proper depth testing is crucial for TBR performance: + +* *Enable Depth Testing and Writing*: This allows the GPU to cull hidden primitives and reduce overdraw. + +* *Avoid Operations That Disable Early-Z*: The following operations can prevent effective early depth testing: + - Using the discard instruction in fragment shaders + - Writing to gl_FragDepth (GLSL) SV_Depth (slang) explicitly + - Using storage images or storage buffers + - Using gl_SampleMask (GLSL explicit way to turn on/off specific pixels) + - Enabling both depth bounds and depth write + - Enabling both blending and depth write + +* *Consistent Compare Operations*: When using compareOp, try to keep the values consistent for each draw in the render pass. + +* *Clear Attachments Properly*: Attachments should be cleared at the beginning of the render pass, or when no valid compareOp value is assigned to previous draw calls. + +=== Immediate Mode Rendering (IMR) + +Traditional desktop GPUs and some older mobile GPUs use an immediate mode rendering architecture. + +==== How IMR Works + +1. *Vertex Processing*: Process vertices and assemble primitives. + +2. *Rasterization*: Convert primitives to fragments. + +3. *Fragment Processing*: Process each fragment and write the result directly to the framebuffer in main memory. + +==== Advantages of IMR + +1. *Simplicity*: The rendering model is more straightforward and matches the traditional graphics pipeline. + +2. *Flexibility*: Some algorithms that require reading from the framebuffer are easier to implement. + +==== Optimizing for IMR + +If your target device uses IMR, consider these optimizations: + +1. *Front-to-Back Rendering*: Render opaque objects from front to back to minimize overdraw. + +2. *Early-Z*: Use depth testing to reject fragments early in the pipeline. + +3. *Occlusion Culling*: Implement occlusion culling to avoid rendering objects that won't be visible. + +=== Detecting Rendering Architecture + +Vulkan doesn't provide a direct way to determine if a GPU uses TBR or IMR. However, you can make educated guesses based on the device vendor and model: + +[source,cpp] +---- +bool is_likely_tbr_gpu(vk::PhysicalDevice physical_device) { + vk::PhysicalDeviceProperties props = physical_device.getProperties(); + + // Most mobile GPUs from these vendors use TBR + if (props.vendorID == 0x5143) { // Qualcomm + return true; + } + if (props.vendorID == 0x1010) { // PowerVR (Imagination Technologies) + return true; + } + if (props.vendorID == 0x13B5) { // ARM Mali + return true; + } + if (props.vendorID == 0x19E5) { // Huawei + return true; + } + + // Apple GPUs are also TBR + if (props.vendorID == 0x106B) { // Apple + return true; + } + + // For other vendors, you might need to maintain a list of known TBR GPUs + // or just assume desktop GPUs are IMR and mobile GPUs are TBR + + return false; +} +---- + +=== Adapting to Both Architectures + +The best approach is to design your engine to work well on both TBR and IMR architectures: + +* *Detect the Architecture*: Use heuristics to detect the likely architecture. + +* *Conditional Optimizations*: Apply different optimizations based on the +detected architecture: + +[source,cpp] +---- +void configure_rendering_pipeline(vk::PhysicalDevice physical_device) { + bool is_tbr = is_likely_tbr_gpu(physical_device); + + if (is_tbr) { + // TBR optimizations + use_transient_attachments = true; + prioritize_subpass_dependencies = true; + avoid_framebuffer_reads = true; + } else { + // IMR optimizations + use_front_to_back_sorting = true; + prioritize_early_z = true; + implement_occlusion_culling = true; + } +} +---- + +* *Fallback Strategy*: If you can't determine the architecture, optimize for +TBR, as those optimizations generally don't harm IMR performance significantly. + +=== Best Practices for Both Architectures + +Regardless of the rendering architecture, these practices will help optimize performance: + +1. *Minimize State Changes*: Group draw calls by material to reduce state changes. + +2. *Batch Similar Objects*: Use instancing or batching to reduce draw call overhead. + +3. *Use Appropriate Synchronization*: Use the minimum synchronization required to ensure correct rendering. + +4. *Profile on Target Devices*: Always test your optimizations on actual target devices. + +In the next section, we'll explore Vulkan extensions that can help you optimize performance on mobile devices, particularly those that leverage the tile-based architecture. + +link:03_performance_optimizations.adoc[Previous: Performance Optimizations] | link:05_vulkan_extensions.adoc[Next: Vulkan Extensions for Mobile] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc new file mode 100644 index 00000000..8db4ca5e --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/05_vulkan_extensions.adoc @@ -0,0 +1,394 @@ +:pp: {plus}{plus} + += Mobile Development: Vulkan Extensions + +== Vulkan Extensions for Mobile + +Vulkan's extensibility is one of its greatest strengths, allowing hardware vendors to expose specialized features that can significantly improve performance. For mobile development, several extensions are particularly valuable as they can help optimize for the unique characteristics of mobile GPUs. In this section, we'll explore key Vulkan extensions that can enhance performance on mobile devices. + +=== VK_KHR_dynamic_rendering + +Dynamic rendering is a game-changing extension that simplifies the Vulkan rendering workflow by eliminating the need for explicit render pass and framebuffer objects. + +==== Overview + +The `VK_KHR_dynamic_rendering` extension (now part of Vulkan 1.3 core) allows you to begin and end rendering operations directly within a command buffer, without creating render pass and framebuffer objects. This benefits a wide range of platforms (desktop and mobile) because it: + +1. *Simplifies Code*: Reduces the complexity of managing render passes and framebuffers. +2. *Enables More Flexible Rendering*: Makes it easier to implement techniques that don't fit well into the traditional render pass model. +3. *Potentially Lowers API Overhead*: Fewer objects to create and manage can simplify setup; any CPU savings are usually small and workload-dependent. + +==== Implementation (Step-by-step) + +Let's break the setup into a few small, focused steps. + +===== Enable the extension and load entry points + +We first enable the device extension and, if you're not on Vulkan 1.3 core, load the function pointers. + +[source,cpp] +---- +// Enable the extension when creating the device +std::vector device_extensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME +}; + +// Get function pointers (if not using Vulkan 1.3) +PFN_vkCmdBeginRenderingKHR vkCmdBeginRenderingKHR = + reinterpret_cast( + vkGetDeviceProcAddr(device, "vkCmdBeginRenderingKHR")); +PFN_vkCmdEndRenderingKHR vkCmdEndRenderingKHR = + reinterpret_cast( + vkGetDeviceProcAddr(device, "vkCmdEndRenderingKHR")); +---- + +This prepares your device to use dynamic rendering and gives access to the commands needed to begin/end a rendering scope without a traditional render pass. + +===== Describe attachments for this rendering scope + +We define the color and depth attachments and package them into a VkRenderingInfoKHR. Think of this as an inline, one-off description of what would normally be baked into render pass/framebuffer objects. + +[source,cpp] +---- +VkRenderingAttachmentInfoKHR color_attachment{}; +color_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR; +color_attachment.imageView = color_image_view; +color_attachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; +color_attachment.clearValue = clear_value; + +VkRenderingAttachmentInfoKHR depth_attachment{}; +depth_attachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR; +depth_attachment.imageView = depth_image_view; +depth_attachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; +depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +depth_attachment.clearValue = depth_clear_value; + +VkRenderingInfoKHR rendering_info{}; +rendering_info.sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR; +rendering_info.renderArea = render_area; +rendering_info.layerCount = 1; +rendering_info.colorAttachmentCount = 1; +rendering_info.pColorAttachments = &color_attachment; +rendering_info.pDepthAttachment = &depth_attachment; +---- + +Each frame (or subpass-equivalent), you can tweak these descriptors directly (e.g., swapchain views after resize), avoiding pipeline-wide re-creation. + +===== Begin rendering, draw, end rendering + +With the attachments described, we open the rendering scope, record draws, then close the scope. + +[source,cpp] +---- +vkCmdBeginRenderingKHR(command_buffer, &rendering_info); + +// Record drawing commands +vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); +vkCmdDraw(command_buffer, vertex_count, 1, 0, 0); + +// End rendering +vkCmdEndRenderingKHR(command_buffer); +---- + +The begin/end pair replaces vkCmdBeginRenderPass/vkCmdEndRenderPass while providing more flexibility for modern rendering flows. + +===== C++ bindings (vulkan.hpp) variant + +If you're using vulkan.hpp (vk::), the structure population is more ergonomic but follows the same steps. + +[source,cpp] +---- +// Using vulkan.hpp +vk::RenderingAttachmentInfoKHR color_attachment; +color_attachment.setImageView(color_image_view); +color_attachment.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal); +color_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +color_attachment.setStoreOp(vk::AttachmentStoreOp::eStore); +color_attachment.setClearValue(clear_value); + +vk::RenderingAttachmentInfoKHR depth_attachment; +depth_attachment.setImageView(depth_image_view); +depth_attachment.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal); +depth_attachment.setLoadOp(vk::AttachmentLoadOp::eClear); +depth_attachment.setStoreOp(vk::AttachmentStoreOp::eDontCare); +depth_attachment.setClearValue(depth_clear_value); + +vk::RenderingInfoKHR rendering_info; +rendering_info.setRenderArea(render_area); +rendering_info.setLayerCount(1); +rendering_info.setColorAttachments(color_attachment); +rendering_info.setPDepthAttachment(&depth_attachment); +---- + +Once the description is assembled, begin the rendering scope, submit draws, and end the scope. + +[source,cpp] +---- +command_buffer.beginRenderingKHR(rendering_info); + +// Record drawing commands +command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); +command_buffer.draw(vertex_count, 1, 0, 0); + +// End rendering +command_buffer.endRenderingKHR(); +---- + +=== VK_KHR_dynamic_rendering_local_read + +The `VK_KHR_dynamic_rendering_local_read` extension is particularly valuable for tile-based renderers as it allows shaders to read from attachments without forcing a tile to main memory and back. + +==== Overview + +This extension enhances dynamic rendering by allowing fragment shaders to read from color and depth/stencil attachments within the same rendering scope. On tile-based renderers, this means the reads can happen directly from tile memory, avoiding expensive round trips to main memory. + +Key benefits include: + +1. *Reduced Memory Bandwidth*: Reads happen from on-chip memory rather than main memory, reducing bandwidth usage for bandwidth-intensive operations. +2. *Improved Performance*: Particularly for algorithms that need to read from previously written attachments. +3. *Power Efficiency*: Lower memory bandwidth means lower power consumption. + +==== How It Reduces Memory Bandwidth + +The `VK_KHR_dynamic_rendering_local_read` extension is particularly effective at reducing memory bandwidth because: + +1. *Eliminates Tile Flush Operations*: Without this extension, when a shader needs to read from a previously written attachment, the GPU must flush the entire tile to main memory and then read it back. This extension allows the shader to read directly from the tile memory, eliminating these costly flush operations. + +2. *Supports Per-Pixel Local Reads*: It enables fragment shaders to read the value written at the same pixel from attachments within the current rendering scope/tile. This suits per-pixel operations (e.g., tone mapping or reading depth/previous color). + +3. *Bandwidth Reduction Measurements*: In real-world applications, this extension has been shown to reduce memory bandwidth for workloads that benefit from per-pixel local reads. The benefit is workload- and GPU-dependent. + +4. *Practical Example*: Consider a deferred rendering pipeline that needs to read G-buffer data at the same pixel for lighting. Without this extension, the G-buffer would need to be written to main memory and then read back for the lighting pass. With this extension, the lighting pass can read directly from the G-buffer in tile memory, saving bandwidth. + +==== Implementation + +To use this extension: + +[source,cpp] +---- +// Enable the extension when creating the device +std::vector device_extensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME +}; + +// Create a pipeline that reads from attachments +vk::PipelineRenderingCreateInfoKHR rendering_create_info; +rendering_create_info.setColorAttachmentCount(1); +rendering_create_info.setColorAttachmentFormats(color_format); +rendering_create_info.setDepthAttachmentFormat(depth_format); + +// Set up the attachment local read info +vk::AttachmentSampleCountInfoAMD sample_count_info; +sample_count_info.setColorAttachmentSamples(vk::SampleCountFlagBits::e1); +sample_count_info.setDepthStencilAttachmentSamples(vk::SampleCountFlagBits::e1); + +vk::RenderingAttachmentLocationInfoKHR location_info; +location_info.setColorAttachmentLocations(0); // Location 0 for the color attachment + +vk::RenderingInputAttachmentIndexInfoKHR input_index_info; +input_index_info.setColorInputAttachmentIndices(0); // Index 0 for the color attachment + +// Create the graphics pipeline +vk::GraphicsPipelineCreateInfo pipeline_info; +pipeline_info.setPNext(&rendering_create_info); +// ... set other pipeline creation parameters + +// In your fragment shader, you can now read from the attachment +// using subpassLoad() or texture sampling with the appropriate extension +// Fragment shader example (GLSL): +// #extension GL_EXT_shader_tile_image : require +// layout(location = 0) out vec4 outColor; +// layout(input_attachment_index = 0, set = 0, binding = 0) uniform subpassInput inputColor; +// void main() { +// vec4 color = subpassLoad(inputColor); +// outColor = color * 2.0; // Double the brightness +// } +---- + +=== VK_EXT_shader_tile_image + +The `VK_EXT_shader_tile_image` extension provides direct access to tile memory in shaders, which can significantly improve performance on tile-based renderers. + +==== Overview + +This extension allows shaders to: + +1. *Access Tile Memory Directly*: Read and write to the current tile's memory without going through main memory. +2. *Perform Tile-Local Operations*: Execute operations that stay entirely within the tile memory. +3. *Optimize Bandwidth-Intensive Algorithms*: Particularly beneficial for post-processing effects. +4. *Reduce Memory Bandwidth*: Helps lower memory bandwidth by keeping data in tile-local memory during multi-pass workloads. + +==== How It Reduces Memory Bandwidth + +The `VK_EXT_shader_tile_image` extension is particularly effective at reducing memory bandwidth for these reasons: + +1. *Tile-Based Architecture Optimization*: Mobile GPUs typically use tile-based rendering, where the screen is divided into small tiles that are processed independently. This extension takes full advantage of this architecture by allowing shaders to work directly with the tile data in fast on-chip memory. + +2. *Eliminates Intermediate Memory Transfers*: Without this extension, multi-pass rendering requires writing results to main memory after each pass and reading them back for the next pass. With `VK_EXT_shader_tile_image`, these intermediate results can stay in tile memory, eliminating these costly transfers. + +3. *Bandwidth Savings Measurements*: Testing on various mobile GPUs has shown meaningful bandwidth reductions for complex multi-pass pipelines; actual gains are workload- and GPU-dependent. + +4. *Practical Applications*: + - *Image Processing Filters*: Applying multiple filters (blur, sharpen, color correction) can be done without leaving tile memory. + - *Deferred Rendering*: G-buffer data can be kept in tile memory for the lighting pass. + - *Shadow Mapping*: Shadow calculations can be performed more efficiently by keeping depth information in tile memory. + +5. *Power Efficiency*: The reduction in memory bandwidth directly translates to lower power consumption, which is critical for mobile devices. Tests have shown up to 20% power savings for graphics-intensive applications. + +==== Implementation + +To use this extension: + +[source,cpp] +---- +// Enable the extension when creating the device +std::vector device_extensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME +}; + +// When creating your shader module, make sure your shader uses the extension +// GLSL example: +// #extension GL_EXT_shader_tile_image : require +// +// layout(tile_image, set = 0, binding = 0) uniform tileImageColor { vec4 color; } tileColor; +// +// void main() { +// // Read from tile memory +// vec4 current_color = tileColor.color; +// +// // Process the color +// vec4 new_color = process(current_color); +// +// // Write back to tile memory +// tileColor.color = new_color; +// } +---- + +=== Combining Extensions for Maximum Performance + +For the best mobile performance, consider using these extensions together: + +[source,cpp] +---- +// Enable all relevant extensions +std::vector device_extensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME, + VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME +}; + +// Check which extensions are supported +auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); +std::vector supported_extensions; + +for (const auto& requested_ext : device_extensions) { + for (const auto& available_ext : available_extensions) { + if (strcmp(requested_ext, available_ext.extensionName) == 0) { + supported_extensions.push_back(requested_ext); + break; + } + } +} + +// Create device with supported extensions +vk::DeviceCreateInfo device_create_info; +device_create_info.setPEnabledExtensionNames(supported_extensions); +// ... set other device creation parameters +vk::Device device = physical_device.createDevice(device_create_info); + +// Now you can use the supported extensions in your rendering code +// ... +---- + +=== Device Extension Support + +Different mobile vendors and devices vary in which Vulkan extensions they expose. Understanding per-device support helps you pick features safely at runtime. + +==== Device Extension Support Details + +Different mobile GPU vendors have varying levels of support for Vulkan extensions: + +* *Dynamic Rendering Support*: Many mobile GPUs have optimized +implementations of `VK_KHR_dynamic_rendering`. This can lead to significant performance improvements compared to traditional render passes, especially on tile-based renderers. + +* *Tile-Based Optimizations*: On tile-based GPUs (e.g., Mali, PowerVR), `VK_EXT_shader_tile_image` and `VK_KHR_dynamic_rendering_local_read` are effective because they keep reads and writes in tile memory. See the extension sections above for details; benefits are workload- and GPU-dependent. + +* *Checking for Extension Support (EXT/KHR) on the current device*: + +[source,cpp] +---- +// Common vendor IDs (used here only for labeling/logging output) +const uint32_t VENDOR_ID_QUALCOMM = 0x5143; // Adreno +const uint32_t VENDOR_ID_ARM = 0x13B5; // Mali +const uint32_t VENDOR_ID_IMAGINATION = 0x1010; // PowerVR +const uint32_t VENDOR_ID_HUAWEI = 0x19E5; // Kirin +const uint32_t VENDOR_ID_APPLE = 0x106B; // Apple + +bool log_device_extension_support(vk::PhysicalDevice physical_device) { + vk::PhysicalDeviceProperties props = physical_device.getProperties(); + std::string vendor_name; + + // Identify vendor for display purposes only + switch (props.vendorID) { + case VENDOR_ID_QUALCOMM: vendor_name = "Qualcomm"; break; + case VENDOR_ID_ARM: vendor_name = "ARM Mali"; break; + case VENDOR_ID_IMAGINATION: vendor_name = "PowerVR"; break; + case VENDOR_ID_HUAWEI: vendor_name = "Huawei"; break; + case VENDOR_ID_APPLE: vendor_name = "Apple"; break; + default: vendor_name = "Unknown"; break; + } + + // Check for widely useful EXT/KHR extensions on this device + auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); + bool has_dynamic_rendering = false; + bool has_dynamic_rendering_local_read = false; + bool has_shader_tile_image = false; + + for (const auto& ext : available_extensions) { + std::string ext_name = ext.extensionName; + if (ext_name == VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME) { + has_dynamic_rendering = true; + } else if (ext_name == VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME) { + has_dynamic_rendering_local_read = true; + } else if (ext_name == VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME) { + has_shader_tile_image = true; + } + } + + // Log the extension support + std::cout << vendor_name << " device detected with extension support:" << std::endl; + std::cout << " Dynamic Rendering: " << (has_dynamic_rendering ? "Yes" : "No") << std::endl; + std::cout << " Dynamic Rendering Local Read: " << (has_dynamic_rendering_local_read ? "Yes" : "No") << std::endl; + std::cout << " Shader Tile Image: " << (has_shader_tile_image ? "Yes" : "No") << std::endl; + + return has_dynamic_rendering || has_dynamic_rendering_local_read || has_shader_tile_image; +} +---- + +* *Platform-Specific Optimizations*: When developing for mobile devices, +consider these optimizations: + - Prioritize the use of dynamic rendering over traditional render passes on tile-based renderers + - Use tile-based extensions whenever available + - Test different configurations to find the optimal settings for various device models + +=== Best Practices for Using Extensions + +1. *Check for Support*: Always check if an extension is supported before using it. + +2. *Fallback Paths*: Implement fallback paths for when extensions aren't available. + +3. *Test on Real Devices*: Extensions may behave differently across vendors and devices. Test on a variety of hardware from different manufacturers. + +4. *Stay Updated*: Keep track of new extensions that could benefit mobile performance, as mobile GPU vendors continue to enhance their Vulkan support. + +In the next section, we'll conclude our exploration of mobile development with a summary of key takeaways and best practices. + +link:04_rendering_approaches.adoc[Previous: Rendering Approaches] | link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc new file mode 100644 index 00000000..fca97fb2 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/06_conclusion.adoc @@ -0,0 +1,201 @@ +:pp: {plus}{plus} + += Mobile Development: Conclusion + +== Conclusion + +=== Putting It All Together + +Let's see how all these components can work together in a complete mobile-optimized Vulkan application: + +[source,cpp] +---- +class MobileOptimizedEngine { +public: + MobileOptimizedEngine() { + // Initialize platform-specific components + #ifdef __ANDROID__ + initialize_android(); + #elif defined(__APPLE__) + initialize_ios(); + #else + initialize_desktop(); + #endif + + // Initialize Vulkan with mobile optimizations + initialize_vulkan(); + } + + void run() { + // Main application loop + while (!should_close()) { + handle_platform_events(); + update(); + render(); + } + + cleanup(); + } + +private: + void initialize_vulkan() { + // Create instance + vk::InstanceCreateInfo instance_info; + // ... set instance parameters + instance = vk::createInstance(instance_info); + + // Select physical device + physical_device = select_physical_device(instance); + + // Detect if we're on a TBR GPU + is_tbr_gpu = is_likely_tbr_gpu(physical_device); + + // Check for extension support + auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); + std::vector supported_extensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + + // Add mobile-specific extensions if supported + if (check_extension_support(available_extensions, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME)) { + supported_extensions.push_back(VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME); + use_dynamic_rendering = true; + } + + if (check_extension_support(available_extensions, VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME)) { + supported_extensions.push_back(VK_KHR_DYNAMIC_RENDERING_LOCAL_READ_EXTENSION_NAME); + use_dynamic_rendering_local_read = true; + } + + if (check_extension_support(available_extensions, VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME)) { + supported_extensions.push_back(VK_EXT_SHADER_TILE_IMAGE_EXTENSION_NAME); + use_shader_tile_image = true; + } + + // Create logical device with supported extensions + vk::DeviceCreateInfo device_info; + device_info.setPEnabledExtensionNames(supported_extensions); + // ... set other device parameters + device = physical_device.createDevice(device_info); + + // Initialize other Vulkan resources + // ... + } + + void render() { + // Begin frame + auto cmd_buffer = begin_frame(); + + if (use_dynamic_rendering) { + // Use dynamic rendering + vk::RenderingAttachmentInfoKHR color_attachment; + // ... set attachment parameters + + vk::RenderingInfoKHR rendering_info; + // ... set rendering parameters + + cmd_buffer.beginRenderingKHR(rendering_info); + + // Record drawing commands + // ... + + cmd_buffer.endRenderingKHR(); + } else { + // Use traditional render passes + // ... + } + + // End frame + end_frame(cmd_buffer); + } + + // Platform-specific initialization + void initialize_android() { + // Android-specific setup + // ... + } + + void initialize_ios() { + // iOS-specific setup with MoltenVK + // ... + } + + void initialize_desktop() { + // Desktop-specific setup + // ... + } + + // Helper functions + bool check_extension_support(const std::vector& available, const char* extension_name) { + for (const auto& ext : available) { + if (strcmp(extension_name, ext.extensionName) == 0) { + return true; + } + } + return false; + } + + bool is_likely_tbr_gpu(vk::PhysicalDevice device) { + vk::PhysicalDeviceProperties props = device.getProperties(); + + // Most mobile GPUs from these vendors use TBR + if (props.vendorID == 0x5143 || // Qualcomm + props.vendorID == 0x1010 || // PowerVR + props.vendorID == 0x13B5 || // ARM Mali + props.vendorID == 0x19E5 || // Huawei + props.vendorID == 0x106B) { // Apple + return true; + } + + return false; + } + + // Vulkan objects + vk::Instance instance; + vk::PhysicalDevice physical_device; + vk::Device device; + + // Flags + bool is_tbr_gpu = false; + bool use_dynamic_rendering = false; + bool use_dynamic_rendering_local_read = false; + bool use_shader_tile_image = false; +}; +---- + +=== Ship-Ready Checklist + +1. Feature detection and fallbacks: Probe EXT/KHR support at startup, enable conditionally, and maintain tested fallback paths. +2. Render path selection: Switch between TBR-friendly and IMR-neutral paths at runtime based on a simple vendor/heuristic check. +3. Framebuffer read policy: Prefer tile-local, per-pixel reads (input attachments or dynamic rendering local read). Avoid patterns that force external memory round-trips. +4. Textures and assets: Use KTX2 as the container; prefer ASTC when available with ETC2/PVRTC fallbacks as needed. Generate mipmaps offline. +5. Memory/attachments: Use transient attachments where results aren’t needed after the pass; suballocate to minimize fragmentation. +6. Thermal/perf governor: Implement dynamic resolution or quality tiers and sensible FPS caps to keep thermals in check. +7. Instrumentation: Add GPU markers/timestamps, frame-time histograms, and bandwidth proxies to track regressions. +8. Device matrix: Maintain a small, representative device lab (different vendors/tiers) and run sanity scenes regularly. + +=== Validation and Profiling Playbook + +- Validate correctness: + * Swapchain details (present mode, min image count) per device. + * Layout transitions and access masks, especially when using local read. + * Synchronization between rendering scopes and compute/transfer work. +- Profile efficiently: + * Use platform tools (e.g., Android GPU Inspector, RenderDoc, Xcode GPU Capture) to identify tile flushes, overdraw, and bandwidth hot spots. + * A/B test: classic render pass vs dynamic rendering, local read on/off, tile-image on/off. + * Track power and thermals over multi‑minute runs, not just single frames. + +=== Next Steps + +- Integrate a capability layer that exposes feature bits (dynamic rendering, local read, tile image) to higher-level systems. +- 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. + +=== Code Examples + +The complete code for this chapter can be found in the following files: + +link:../../attachments/simple_engine/36_mobile_platform_integration.cpp[Mobile Platform Integration C{pp} code] +link:../../attachments/simple_engine/37_mobile_optimizations.cpp[Mobile Optimizations C{pp} code] +link:../../attachments/simple_engine/38_tbr_optimizations.cpp[TBR Optimizations C{pp} code] +link:../../attachments/simple_engine/39_mobile_extensions.cpp[Mobile Extensions C{pp} code] + +xref:05_vulkan_extensions.adoc[Previous: Vulkan Extensions for Mobile] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Mobile_Development/index.adoc b/en/Building_a_Simple_Engine/Mobile_Development/index.adoc new file mode 100644 index 00000000..d4975d67 --- /dev/null +++ b/en/Building_a_Simple_Engine/Mobile_Development/index.adoc @@ -0,0 +1,15 @@ +:pp: {plus}{plus} + += Mobile Development + + +This chapter covers the essential aspects of adapting your Vulkan engine for mobile platforms, focusing on Android and iOS development, performance optimizations, rendering approaches, and mobile-specific Vulkan extensions. + +* link:01_introduction.adoc[Introduction] +* link:02_platform_considerations.adoc[Platform Considerations for Android and iOS] +* link:03_performance_optimizations.adoc[Performance Optimizations for Mobile] +* link:04_rendering_approaches.adoc[Rendering Approaches: TBR vs IMR] +* link:05_vulkan_extensions.adoc[Vulkan Extensions for Mobile] +* link:06_conclusion.adoc[Conclusion] + +link:../Tooling/07_conclusion.adoc[Previous: Tooling Conclusion] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc new file mode 100644 index 00000000..28a0fbff --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/01_introduction.adoc @@ -0,0 +1,76 @@ +:pp: {plus}{plus} + += Subsystems: Introduction + +== Introduction to Engine Subsystems + +In previous chapters, we've built the foundation of our simple engine, implementing core components like the rendering pipeline, camera systems, and model loading. Now, we're ready to expand our engine's capabilities by adding two critical subsystems: Audio and Physics. + +These subsystems are essential for creating immersive and interactive experiences in modern games and simulations. While they may seem separate from the graphics pipeline we've been focusing on, modern engines can leverage Vulkan's computational power to enhance both audio processing and physics simulations. + +=== What We'll Cover + +This chapter will take you through implementing two crucial engine subsystems that bring games and simulations to life. We'll begin with an audio subsystem, starting from the fundamentals of playing sounds and music, then advancing to sophisticated techniques like Head-Related Transfer Function (HRTF) processing for convincing 3D spatial audio. The progression shows how Vulkan compute shaders can transform basic audio playback into immersive soundscapes that respond to your 3D world. + +Our physics subsystem follows a similar path, beginning with essential collision detection and response mechanisms that make objects interact believably. As we develop these foundations, we'll demonstrate how Vulkan's parallel processing capabilities can accelerate physics calculations dramatically, enabling simulations with large numbers of interacting objects that would overwhelm traditional CPU-based approaches. + +Throughout this chapter, we'll continue our modern C++ approach from previous chapters. + +=== Why Vulkan for Audio and Physics? + +The decision to use Vulkan for audio processing and physics simulations might seem unconventional at first, but it represents a forward-thinking approach to engine development that leverages modern hardware capabilities. + +Modern GPUs provide massive parallel processing power through thousands of cores designed for simultaneous computation. Through Vulkan's compute shaders, we can harness this computational muscle for tasks far beyond graphics rendering. Audio processing benefits tremendously from parallel operations—imagine processing dozens of simultaneous sound sources with real-time spatial effects, or running complex physics simulations with thousands of interacting objects. + +Vulkan's unified memory model creates opportunities for efficiency that traditional separated approaches cannot match. When graphics, audio, and physics processing share memory spaces, we eliminate the costly data transfers that would otherwise shuttle information between CPU and GPU repeatedly. This shared memory architecture enables sophisticated interactions—physics simulations can directly influence particle systems, audio processing can respond to visual effects, and all systems can work together seamlessly. + +Cross-platform consistency becomes increasingly valuable as projects target multiple devices. By implementing these subsystems through Vulkan, we maintain identical behavior across Windows, Linux, mobile platforms, and emerging devices. This consistency reduces debugging time and ensures that audio and physics behavior remains predictable regardless of deployment target. + +The performance benefits extend beyond raw computational power. Offloading intensive calculations to the GPU frees CPU resources for game logic, scripting, AI processing, and other tasks that require sequential processing or complex branching. This separation allows each processor type to focus on tasks it handles most efficiently. + +Additionally, the intention here is to offer a perspective of using Vulkan +for more than just Graphics in your application. Our goal with this tutorial + isn't to provide you a production quality game engine. It's to provide you + with the tools necessary to tackle any Vulkan application development and to + think critically about how your applications can benefit from the GPU. + +=== Practical considerations: Don't offload everything to the GPU + +While Vulkan compute can deliver impressive speedups, it's not always advisable to offload every subsystem to the GPU—especially on mobile: + +* Mobile power and thermals: Many phones and tablets use big.LITTLE CPU clusters and mobile GPUs that are power/thermal constrained. Sustained heavy GPU compute can quickly lead to thermal throttling, causing frame rate drops and inconsistent latency. +* Scheduling and latency: GPUs excel at throughput, but certain tasks (tight control loops, small pointer-heavy updates) can prefer CPU execution due to launch overheads and scheduling latency. +* Determinism and debugging: For gameplay-critical physics, determinism and step-by-step debugging on the CPU can be advantageous. Consider keeping broad-phase or whole-physics on the CPU on mobile, or use a hybrid approach (e.g., CPU broad-phase + GPU narrow-phase). +* Memory bandwidth: On integrated architectures, GPU/CPU share memory bandwidth. Aggressively moving everything to GPU can contend with graphics and hurt frame time and battery life. + +Audio-specific guidance: + +* Prefer platform audio APIs and any available dedicated audio hardware/DSP when feasible (for mixing, resampling, effects). This path often provides lower latency, better power characteristics, and more predictable behavior across devices. +* HRTF support in dedicated hardware is not widespread in the wild. Many consumer devices rely on software HRTF in the OS or application. Evaluate your needs: a software HRTF pipeline may be perfectly adequate; reserving GPU compute strictly for spatial audio is rarely necessary unless you have many sources or complex effects. + +Practical recommendations: + +* Profile first: Establish CPU and GPU baselines before moving work. +* Favor hybrid designs: Offload the clearly parallel, heavy kernels (e.g., batched constraint solves, FFT/IR convolution) while keeping control/coordination on CPU. +* Plan for mobility: Provide runtime toggles to switch between CPU/GPU paths based on device class, thermal state, and power mode. + +=== Prerequisites + +This chapter builds extensively on the engine architecture and Vulkan foundations established in previous chapters. The modular design patterns we've implemented become crucial when adding subsystems that need to integrate cleanly with existing rendering, camera, and resource management systems. + +Experience with Vulkan compute shaders is essential, as we'll leverage compute capabilities to accelerate both audio processing and physics calculations. If you haven't worked through the compute shader sections in the main tutorial, review them before proceeding—the parallel processing concepts and GPU memory management techniques translate directly to our subsystem implementations. + +A basic understanding of audio and physics concepts in game development will help you appreciate the design decisions we make throughout the implementation. While we'll explain the fundamentals as we build each system, familiarity with concepts like sound attenuation, collision detection, and rigid body dynamics will deepen your understanding of how these subsystems serve the broader goals of interactive applications. + +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] +* xref:../../11_Compute_Shader.adoc[Compute shaders] + +Let's begin by exploring how to implement a basic audio subsystem and then enhance it with Vulkan's computational capabilities. + +link:../Loading_Models/09_conclusion.adoc[Previous: Loading Models Conclusion] | link:02_audio_basics.adoc[Next: Audio Basics] diff --git a/en/Building_a_Simple_Engine/Subsystems/02_audio_basics.adoc b/en/Building_a_Simple_Engine/Subsystems/02_audio_basics.adoc new file mode 100644 index 00000000..afc05d08 --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/02_audio_basics.adoc @@ -0,0 +1,243 @@ +::pp: {plus}{plus} + += Subsystems: Audio Basics +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c++ + +== Audio System Fundamentals + +Before we dive into how Vulkan can enhance audio processing, let's establish a foundation by implementing a basic audio system for our engine. This will give us a framework that we can later extend with Vulkan compute capabilities. + +=== Audio System Architecture + +A typical game audio system consists of several key components: + +* *Audio Engine*: The core component that manages audio playback, mixing, and effects processing. +* *Sound Resources*: Audio files loaded into memory and prepared for playback. +* *Audio Channels*: Logical paths for audio to flow through, often grouped by type (e.g., music, sound effects, dialogue). +* *Spatial Audio*: System for positioning sounds in 3D space relative to the listener. +* *Effects Processing*: Application of effects like reverb, echo, or equalization to audio streams. + +Let's implement a simple audio system that covers these basics, using a modern C++ approach consistent with our engine's design. + +=== Basic Audio System Implementation + +We'll start by defining the core classes for our audio system: + +[source,cpp] +---- +// Audio.h +#pragma once + +#include +#include +#include +#include +#include + +namespace Engine { +namespace Audio { + +class AudioClip { +public: + AudioClip(const std::string& filename); + ~AudioClip(); + + // Get raw audio data + const float* GetData() const { return m_Data.data(); } + size_t GetSampleCount() const { return m_Data.size(); } + int GetChannelCount() const { return m_ChannelCount; } + int GetSampleRate() const { return m_SampleRate; } + +private: + std::vector m_Data; + int m_ChannelCount; + int m_SampleRate; +}; + +class AudioSource { +public: + AudioSource(); + ~AudioSource(); + + void SetClip(std::shared_ptr clip) { m_Clip = clip; } + void SetPosition(const glm::vec3& position) { m_Position = position; } + void SetVolume(float volume) { m_Volume = volume; } + void SetLooping(bool looping) { m_Looping = looping; } + + void Play(); + void Stop(); + void Pause(); + + bool IsPlaying() const { return m_IsPlaying; } + + const glm::vec3& GetPosition() const { return m_Position; } + float GetVolume() const { return m_Volume; } + +private: + std::shared_ptr m_Clip; + glm::vec3 m_Position = glm::vec3(0.0f); + float m_Volume = 1.0f; + bool m_Looping = false; + bool m_IsPlaying = false; + + // Implementation-specific playback state + size_t m_CurrentSample = 0; +}; + +class AudioListener { +public: + void SetPosition(const glm::vec3& position) { m_Position = position; } + void SetOrientation(const glm::vec3& forward, const glm::vec3& up) { + m_Forward = forward; + m_Up = up; + } + + const glm::vec3& GetPosition() const { return m_Position; } + const glm::vec3& GetForward() const { return m_Forward; } + const glm::vec3& GetUp() const { return m_Up; } + +private: + glm::vec3 m_Position = glm::vec3(0.0f); + glm::vec3 m_Forward = glm::vec3(0.0f, 0.0f, -1.0f); + glm::vec3 m_Up = glm::vec3(0.0f, 1.0f, 0.0f); +}; + +class AudioSystem { +public: + AudioSystem(); + ~AudioSystem(); + + void Initialize(); + void Shutdown(); + + // Update audio system (call once per frame) + void Update(float deltaTime); + + // Resource management + std::shared_ptr LoadClip(const std::string& name, const std::string& filename); + std::shared_ptr GetClip(const std::string& name); + + // Source management + std::shared_ptr CreateSource(); + void DestroySource(std::shared_ptr source); + + // Listener (typically attached to camera) + AudioListener& GetListener() { return m_Listener; } + +private: + std::unordered_map> m_Clips; + std::vector> m_Sources; + AudioListener m_Listener; + + // Implementation-specific audio backend state + void* m_AudioBackend = nullptr; +}; + +} // namespace Audio +} // namespace Engine +---- + +This basic structure provides a foundation for loading and playing audio files with spatial positioning. In a real implementation, you would integrate with an audio library like OpenAL, FMOD, or Wwise to handle the low-level audio playback. + +=== Integrating with the Engine + +To integrate our audio system with the rest of our engine, we'll add it to our engine's main class: + +[source,cpp] +---- +// Engine.h +#include "Audio.h" + +namespace Engine { + +class Engine { +public: + // ... existing engine code ... + + Audio::AudioSystem& GetAudioSystem() { return m_AudioSystem; } + +private: + // ... existing engine members ... + + Audio::AudioSystem m_AudioSystem; +}; + +} // namespace Engine +---- + +And we'll initialize it during engine startup: + +[source,cpp] +---- +// Engine.cpp +void Engine::Initialize() { + // ... existing initialization code ... + + m_AudioSystem.Initialize(); +} + +void Engine::Shutdown() { + m_AudioSystem.Shutdown(); + + // ... existing shutdown code ... +} +---- + +=== Basic Usage Example + +Here's how you might use this audio system in a game: + +[source,cpp] +---- +// Game code +void Game::LoadResources() { + // Load audio clips + auto explosionSound = m_Engine.GetAudioSystem().LoadClip("explosion", "sounds/explosion.wav"); + auto backgroundMusic = m_Engine.GetAudioSystem().LoadClip("music", "sounds/background.ogg"); + + // Create and configure audio sources + m_MusicSource = m_Engine.GetAudioSystem().CreateSource(); + m_MusicSource->SetClip(backgroundMusic); + m_MusicSource->SetLooping(true); + m_MusicSource->SetVolume(0.5f); + m_MusicSource->Play(); +} + +void Game::OnExplosion(const glm::vec3& position) { + // Create a temporary source for the explosion sound + auto source = m_Engine.GetAudioSystem().CreateSource(); + source->SetClip(m_Engine.GetAudioSystem().GetClip("explosion")); + source->SetPosition(position); + source->Play(); + + // In a real implementation, you'd need to manage the lifetime of this source +} + +void Game::Update(float deltaTime) { + // Update listener position and orientation based on camera + auto& listener = m_Engine.GetAudioSystem().GetListener(); + listener.SetPosition(m_Camera.GetPosition()); + listener.SetOrientation(m_Camera.GetForward(), m_Camera.GetUp()); + + // Update audio system + m_Engine.GetAudioSystem().Update(deltaTime); +} +---- + +=== Limitations of Basic Audio Systems + +While this basic audio system provides the essential functionality for playing sounds in a game, it has several limitations: + +1. *Limited Spatial Audio*: Basic distance-based attenuation doesn't accurately model how sound propagates in 3D space. +2. *CPU-Intensive Processing*: Effects and 3D audio calculations can consume significant CPU resources. +3. *Limited Scalability*: Processing hundreds or thousands of sound sources can become a performance bottleneck. + +In the next section, we'll explore how Vulkan compute shaders can address these limitations by offloading audio processing to the GPU, particularly for implementing more realistic spatial audio through Head-Related Transfer Functions (HRTF). + +link:01_introduction.adoc[Previous: Introduction] | link:03_vulkan_audio.adoc[Next: Vulkan for Audio Processing] diff --git a/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc b/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc new file mode 100644 index 00000000..55b94585 --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/03_vulkan_audio.adoc @@ -0,0 +1,434 @@ +:pp: {plus}{plus} + += Subsystems: Vulkan for Audio Processing + +== Enhancing Audio with Vulkan + +In the previous section, we implemented a basic audio system for our engine. Now, we'll explore how Vulkan's compute capabilities can enhance audio processing, particularly for implementing realistic 3D spatial audio using Head-Related Transfer Functions (HRTF). + +=== Understanding HRTF + +Head-Related Transfer Functions (HRTF) are a set of acoustic filters that model how sound is altered by the head, outer ear, and torso before reaching the eardrums. These filters vary based on the direction of the sound source relative to the listener. + +HRTF processing allows us to create convincing 3D audio by applying the appropriate filters to sound sources based on their position. This creates a more immersive experience than simple stereo panning and distance attenuation. + +The challenge with HRTF processing is that it's computationally expensive: + +1. Each sound source requires a unique set of filters based on its position +2. These filters must be applied to the audio stream in real-time +3. The process involves complex convolutions (multiplying audio samples with filter coefficients) + +This is where Vulkan compute shaders can help by offloading these calculations to the GPU. + +[[audio-vulkan-why]] +=== Why Use Vulkan for Audio Processing? + +Traditional audio processing is done on the CPU, but there are several advantages to using Vulkan compute shaders for certain audio tasks: + +1. *Parallelism*: Audio processing, especially HRTF convolution, can be highly parallelized, making it well-suited for GPU computation. +2. *Reduced CPU Load*: Offloading audio processing to the GPU frees up CPU resources for game logic, AI, and other tasks. +3. *Scalability*: GPU-based processing can more easily scale to handle hundreds or thousands of simultaneous sound sources. +4. *Unified Memory*: With Vulkan, we can share memory between graphics and audio processing, reducing data transfer overhead. + +=== Implementing HRTF Processing with Vulkan + +Let's extend our audio system to include HRTF processing using Vulkan compute shaders. + +First, we'll add HRTF-related structures to our audio system: + +[source,cpp] +---- +// Audio.h (additions) +#include +#include + +namespace Engine { +namespace Audio { + +// HRTF data for a specific direction +struct HRTFData { + std::array leftEarImpulseResponse; + std::array rightEarImpulseResponse; +}; + +// HRTF database containing filters for different directions +class HRTFDatabase { +public: + HRTFDatabase(const std::string& filename); + + // Get HRTF data for a specific direction + const HRTFData& GetHRTFData(float azimuth, float elevation) const; + +private: + // In a real implementation, this would be a more sophisticated data structure + std::vector m_Data; + // Mapping from direction to data index + // ... +}; + +// Extended AudioSystem with Vulkan-based HRTF processing +class AudioSystem { +public: + // ... existing methods ... + + // Enable/disable HRTF processing + void SetHRTFEnabled(bool enabled) { m_HRTFEnabled = enabled; } + bool IsHRTFEnabled() const { return m_HRTFEnabled; } + + // Set the HRTF database to use + void SetHRTFDatabase(std::shared_ptr database) { m_HRTFDatabase = database; } + +private: + // ... existing members ... + + // HRTF processing + bool m_HRTFEnabled = false; + std::shared_ptr m_HRTFDatabase; + + // Vulkan resources for HRTF processing + struct VulkanResources { + vk::raii::ShaderModule computeShaderModule = nullptr; + vk::raii::DescriptorSetLayout descriptorSetLayout = nullptr; + vk::raii::PipelineLayout pipelineLayout = nullptr; + vk::raii::Pipeline computePipeline = nullptr; + vk::raii::DescriptorPool descriptorPool = nullptr; + + // Buffers for audio data + 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; + + // Descriptor sets + std::vector descriptorSets; + + // Command buffer for compute operations + vk::raii::CommandPool commandPool = nullptr; + vk::raii::CommandBuffer commandBuffer = nullptr; + }; + + VulkanResources m_VulkanResources; + + // Initialize Vulkan resources for HRTF processing + void InitializeVulkanResources(); + void CleanupVulkanResources(); + + // Process audio with HRTF using Vulkan + void ProcessAudioWithVulkan(float* inputBuffer, float* outputBuffer, size_t frameCount); +}; + +} // namespace Audio +} // namespace Engine +---- + +Now, let's implement the Vulkan-based HRTF processing: + +[source,cpp] +---- +// Audio.cpp (implementation) + +void AudioSystem::InitializeVulkanResources() { + // Get Vulkan device from the engine + auto& device = m_Engine.GetVulkanDevice(); + + // Create compute shader module + auto shaderCode = LoadShaderFile("shaders/hrtf_processing.comp.spv"); + vk::ShaderModuleCreateInfo shaderModuleCreateInfo({}, shaderCode.size() * sizeof(uint32_t), + reinterpret_cast(shaderCode.data())); + m_VulkanResources.computeShaderModule = vk::raii::ShaderModule(device, shaderModuleCreateInfo); + + // Create descriptor set layout + std::array bindings = { + // Input audio buffer + vk::DescriptorSetLayoutBinding(0, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // Output audio buffer + vk::DescriptorSetLayoutBinding(1, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // HRTF data buffer + vk::DescriptorSetLayoutBinding(2, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute) + }; + + vk::DescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo({}, bindings); + m_VulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(device, descriptorSetLayoutCreateInfo); + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo({}, *m_VulkanResources.descriptorSetLayout); + m_VulkanResources.pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutCreateInfo); + + // Create compute pipeline + vk::PipelineShaderStageCreateInfo shaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.computeShaderModule, "main"); + vk::ComputePipelineCreateInfo computePipelineCreateInfo({}, shaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.computePipeline = vk::raii::Pipeline(device, nullptr, computePipelineCreateInfo); + + // Create descriptor pool + std::array poolSizes = { + vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 3) + }; + vk::DescriptorPoolCreateInfo descriptorPoolCreateInfo({}, 1, poolSizes); + m_VulkanResources.descriptorPool = vk::raii::DescriptorPool(device, descriptorPoolCreateInfo); + + // Allocate descriptor sets + vk::DescriptorSetAllocateInfo descriptorSetAllocateInfo(*m_VulkanResources.descriptorPool, + 1, &*m_VulkanResources.descriptorSetLayout); + m_VulkanResources.descriptorSets = vk::raii::DescriptorSets(device, descriptorSetAllocateInfo); + + // Create buffers for audio data + // In a real implementation, you would size these appropriately and handle multiple frames + CreateBuffer(device, sizeof(float) * 1024, vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.inputBuffer, m_VulkanResources.inputBufferMemory); + CreateBuffer(device, sizeof(float) * 2048, vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.outputBuffer, m_VulkanResources.outputBufferMemory); + CreateBuffer(device, sizeof(float) * 512, vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.hrtfBuffer, m_VulkanResources.hrtfBufferMemory); + + // Update descriptor sets + std::array bufferInfos = { + vk::DescriptorBufferInfo(*m_VulkanResources.inputBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.outputBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.hrtfBuffer, 0, VK_WHOLE_SIZE) + }; + + std::array descriptorWrites = { + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 0, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[0]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 1, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[1]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 2, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[2]) + }; + + device.updateDescriptorSets(descriptorWrites, {}); + + // Create command pool and command buffer + vk::CommandPoolCreateInfo commandPoolCreateInfo({}, m_Engine.GetVulkanQueueFamilyIndex()); + m_VulkanResources.commandPool = vk::raii::CommandPool(device, commandPoolCreateInfo); + + vk::CommandBufferAllocateInfo commandBufferAllocateInfo(*m_VulkanResources.commandPool, + vk::CommandBufferLevel::ePrimary, 1); + auto commandBuffers = vk::raii::CommandBuffers(device, commandBufferAllocateInfo); + m_VulkanResources.commandBuffer = std::move(commandBuffers[0]); +} + +void AudioSystem::ProcessAudioWithVulkan(float* inputBuffer, float* outputBuffer, size_t frameCount) { + if (!m_HRTFEnabled || !m_HRTFDatabase) { + // If HRTF is disabled, just copy input to output (or do simple stereo panning) + memcpy(outputBuffer, inputBuffer, frameCount * sizeof(float)); + return; + } + + auto& device = m_Engine.GetVulkanDevice(); + auto& queue = m_Engine.GetVulkanComputeQueue(); + + // Copy input audio data to the input buffer + void* data; + vkMapMemory(device, *m_VulkanResources.inputBufferMemory, 0, frameCount * sizeof(float), 0, &data); + memcpy(data, inputBuffer, frameCount * sizeof(float)); + vkUnmapMemory(device, *m_VulkanResources.inputBufferMemory); + + // Update HRTF data based on source positions + // In a real implementation, you would update this for each sound source + // For simplicity, we're just using a single HRTF filter here + const auto& hrtfData = m_HRTFDatabase->GetHRTFData(0.0f, 0.0f); + vkMapMemory(device, *m_VulkanResources.hrtfBufferMemory, 0, sizeof(HRTFData), 0, &data); + memcpy(data, &hrtfData, sizeof(HRTFData)); + vkUnmapMemory(device, *m_VulkanResources.hrtfBufferMemory); + + // Record command buffer + vk::CommandBufferBeginInfo beginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); + m_VulkanResources.commandBuffer.begin(beginInfo); + + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, *m_VulkanResources.computePipeline); + m_VulkanResources.commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.pipelineLayout, 0, + *m_VulkanResources.descriptorSets[0], {}); + + // Dispatch compute shader + // The workgroup size should match what's defined in the shader + m_VulkanResources.commandBuffer.dispatch(frameCount / 64 + 1, 1, 1); + + m_VulkanResources.commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo({}, {}, *m_VulkanResources.commandBuffer); + queue.submit(submitInfo, nullptr); + queue.waitIdle(); + + // Copy output audio data from the output buffer + vkMapMemory(device, *m_VulkanResources.outputBufferMemory, 0, frameCount * 2 * sizeof(float), 0, &data); + memcpy(outputBuffer, data, frameCount * 2 * sizeof(float)); + vkUnmapMemory(device, *m_VulkanResources.outputBufferMemory); +} + +void AudioSystem::Update(float deltaTime) { + // Process all active audio sources + for (auto& source : m_Sources) { + if (source->IsPlaying()) { + // Get audio data from the source + auto clip = source->GetClip(); + if (!clip) continue; + + // Calculate spatial position relative to listener + glm::vec3 relativePosition = source->GetPosition() - m_Listener.GetPosition(); + + // Rotate relative position based on listener orientation + glm::mat3 listenerOrientation( + glm::cross(m_Listener.GetForward(), m_Listener.GetUp()), + m_Listener.GetUp(), + -m_Listener.GetForward() + ); + relativePosition = listenerOrientation * relativePosition; + + // Calculate azimuth and elevation + float distance = glm::length(relativePosition); + float azimuth = atan2(relativePosition.x, relativePosition.z); + float elevation = atan2(relativePosition.y, sqrt(relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z)); + + // Get audio data from the clip + const float* audioData = clip->GetData() + source->GetCurrentSample(); + size_t remainingSamples = clip->GetSampleCount() - source->GetCurrentSample(); + size_t framesToProcess = std::min(remainingSamples, size_t(1024)); + + // Process audio with HRTF using Vulkan + float processedAudio[2048]; // Stereo output (2 channels) + ProcessAudioWithVulkan(const_cast(audioData), processedAudio, framesToProcess); + + // Send processed audio to the audio backend + // ... + + // Update source state + source->IncrementSample(framesToProcess); + } + } +} +---- + +=== HRTF Compute Shader + +Here's the compute shader that performs the HRTF convolution: + +[source,glsl] +---- +// hrtf_processing.comp +#version 450 + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +// Input mono audio buffer +layout(std430, binding = 0) buffer InputBuffer { + float samples[]; +} inputBuffer; + +// Output stereo audio buffer +layout(std430, binding = 1) buffer OutputBuffer { + float leftSamples[]; + float rightSamples[]; +} outputBuffer; + +// HRTF data +layout(std430, binding = 2) buffer HRTFBuffer { + float leftImpulseResponse[256]; + float rightImpulseResponse[256]; +} hrtfBuffer; + +void main() { + uint gID = gl_GlobalInvocationID.x; + + // Check if this invocation is within the audio buffer + if (gID >= inputBuffer.samples.length()) { + return; + } + + // Perform convolution with HRTF impulse responses + float leftSample = 0.0; + float rightSample = 0.0; + + for (int i = 0; i < 256; i++) { + int sampleIndex = int(gID) - i; + if (sampleIndex >= 0 && sampleIndex < inputBuffer.samples.length()) { + leftSample += inputBuffer.samples[sampleIndex] * hrtfBuffer.leftImpulseResponse[i]; + rightSample += inputBuffer.samples[sampleIndex] * hrtfBuffer.rightImpulseResponse[i]; + } + } + + // Write to output buffer + outputBuffer.leftSamples[gID] = leftSample; + outputBuffer.rightSamples[gID] = rightSample; +} +---- + +=== Performance Considerations + +When implementing HRTF processing with Vulkan, consider these performance optimizations: + +1. *Batch Processing*: Process multiple audio frames in a single dispatch to amortize the overhead of command submission. +2. *Memory Transfers*: Minimize transfers between CPU and GPU memory by processing larger chunks of audio at once. +3. *Multiple Sources*: Process multiple sound sources in a single shader invocation to maximize GPU utilization. +4. *Dynamic HRTF Selection*: Only update HRTF filters when sound source positions change significantly. +5. *Workgroup Size*: Tune the workgroup size based on your target hardware for optimal performance. + +=== Integration with the Audio System + +To integrate the Vulkan-based HRTF processing into our audio system, we need to modify the `AudioSystem::Initialize` method: + +[source,cpp] +---- +void AudioSystem::Initialize() { + // Initialize audio backend + // ... + + // Initialize Vulkan resources for HRTF processing + if (m_Engine.IsVulkanInitialized()) { + InitializeVulkanResources(); + } + + // Load default HRTF database + m_HRTFDatabase = std::make_shared("data/hrtf/default.hrtf"); + m_HRTFEnabled = true; +} + +void AudioSystem::Shutdown() { + // Cleanup Vulkan resources + if (m_Engine.IsVulkanInitialized()) { + CleanupVulkanResources(); + } + + // Shutdown audio backend + // ... +} +---- + +=== Advantages of Vulkan-Based HRTF + +See the core benefits listed in <> for a summary of why compute shaders are a good fit. In the context of HRTF specifically, two practical advantages are worth highlighting: + +1. *Quality*: You can afford higher-order HRTF filters without significant performance impact, improving spatial realism. +2. *Advanced Effects*: The GPU's compute power enables more sophisticated effects (e.g., room acoustics simulation) alongside HRTF. + +=== Limitations and Considerations + +While Vulkan-based audio processing offers many advantages, there are some limitations to consider: + +1. *Latency*: GPU processing introduces additional latency, which may be problematic for real-time audio. +2. *Complexity*: Implementing and debugging GPU-based audio processing is more complex than CPU-based solutions. +3. *Platform Support*: Not all platforms support Vulkan, so you may need fallback CPU implementations. +4. *Power Consumption*: GPU processing may increase power consumption, which is a consideration for mobile devices. + +=== Real-World Applications + +Several modern game engines and audio middleware solutions are beginning to leverage GPU acceleration for audio processing: + +1. *Steam Audio*: Valve's audio SDK supports GPU acceleration for its spatial audio processing. +2. *Wwise*: Audiokinetic's Wwise can offload certain DSP effects to the GPU. +3. *Custom Solutions*: AAA game studios often implement custom GPU-accelerated audio processing for their titles. + +By implementing Vulkan-based HRTF processing in our engine, we're following industry best practices for high-performance audio in modern games. + +In the next section, we'll shift our focus to the physics subsystem and explore how Vulkan compute shaders can accelerate physics simulations. + +link:02_audio_basics.adoc[Previous: Audio Basics] | link:04_physics_basics.adoc[Next: Physics Basics] diff --git a/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc b/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc new file mode 100644 index 00000000..5ade0ea9 --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/04_physics_basics.adoc @@ -0,0 +1,566 @@ +:pp: {plus}{plus} + += Subsystems: Physics Basics + +== Physics System Fundamentals + +Before we explore how Vulkan can accelerate physics simulations, let's establish a foundation by implementing a basic physics system for our engine. This will give us a framework that we can later enhance with Vulkan compute capabilities. + +=== Physics System Architecture + +A typical game physics system consists of several key components: + +* *Rigid Body Dynamics*: Simulation of solid objects with mass, velocity, and rotational properties. +* *Collision Detection*: Determining when objects intersect or contact each other. +* *Collision Response*: Calculating how objects should react when they collide. +* *Constraints*: Limiting the movement of objects based on joints, hinges, or other connections. +* *Continuous Collision Detection*: Handling fast-moving objects that might pass through others between frames. +* *Spatial Partitioning*: Optimizing collision detection by dividing the world into regions. + +Let's implement a simple physics system that covers these basics, using a modern C++ approach consistent with our engine's design. + +=== Basic Physics System Implementation + +We'll start by defining the core classes for our physics system: + +[source,cpp] +---- +// Physics.h +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Engine { +namespace Physics { + +enum class ColliderType { + Box, + Sphere, + Capsule, + Mesh +}; + +class Collider { +public: + virtual ~Collider() = default; + virtual ColliderType GetType() const = 0; + + void SetOffset(const glm::vec3& offset) { m_Offset = offset; } + const glm::vec3& GetOffset() const { return m_Offset; } + +protected: + glm::vec3 m_Offset = glm::vec3(0.0f); +}; + +class BoxCollider : public Collider { +public: + BoxCollider(const glm::vec3& halfExtents) : m_HalfExtents(halfExtents) {} + + ColliderType GetType() const override { return ColliderType::Box; } + + const glm::vec3& GetHalfExtents() const { return m_HalfExtents; } + void SetHalfExtents(const glm::vec3& halfExtents) { m_HalfExtents = halfExtents; } + +private: + glm::vec3 m_HalfExtents; +}; + +class SphereCollider : public Collider { +public: + SphereCollider(float radius) : m_Radius(radius) {} + + ColliderType GetType() const override { return ColliderType::Sphere; } + + float GetRadius() const { return m_Radius; } + void SetRadius(float radius) { m_Radius = radius; } + +private: + float m_Radius; +}; + +class RigidBody { +public: + RigidBody(); + ~RigidBody(); + + // Kinematic state + void SetPosition(const glm::vec3& position) { m_Position = position; } + void SetRotation(const glm::quat& rotation) { m_Rotation = rotation; } + void SetLinearVelocity(const glm::vec3& velocity) { m_LinearVelocity = velocity; } + void SetAngularVelocity(const glm::vec3& velocity) { m_AngularVelocity = velocity; } + + const glm::vec3& GetPosition() const { return m_Position; } + const glm::quat& GetRotation() const { return m_Rotation; } + const glm::vec3& GetLinearVelocity() const { return m_LinearVelocity; } + const glm::vec3& GetAngularVelocity() const { return m_AngularVelocity; } + + // Physical properties + void SetMass(float mass); + float GetMass() const { return m_Mass; } + float GetInverseMass() const { return m_InverseMass; } + + void SetRestitution(float restitution) { m_Restitution = restitution; } + float GetRestitution() const { return m_Restitution; } + + void SetFriction(float friction) { m_Friction = friction; } + float GetFriction() const { return m_Friction; } + + // Collider management + void SetCollider(std::shared_ptr collider) { m_Collider = collider; } + std::shared_ptr GetCollider() const { return m_Collider; } + + // Forces and impulses + void ApplyForce(const glm::vec3& force); + void ApplyImpulse(const glm::vec3& impulse); + void ApplyTorque(const glm::vec3& torque); + void ApplyTorqueImpulse(const glm::vec3& torqueImpulse); + + // Simulation flags + void SetKinematic(bool kinematic) { m_IsKinematic = kinematic; } + bool IsKinematic() const { return m_IsKinematic; } + + void SetGravityEnabled(bool enabled) { m_UseGravity = enabled; } + bool IsGravityEnabled() const { return m_UseGravity; } + +private: + // Kinematic state + glm::vec3 m_Position = glm::vec3(0.0f); + glm::quat m_Rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); + glm::vec3 m_LinearVelocity = glm::vec3(0.0f); + glm::vec3 m_AngularVelocity = glm::vec3(0.0f); + + // Forces + glm::vec3 m_AccumulatedForce = glm::vec3(0.0f); + glm::vec3 m_AccumulatedTorque = glm::vec3(0.0f); + + // Physical properties + float m_Mass = 1.0f; + float m_InverseMass = 1.0f; + glm::mat3 m_InertiaTensor = glm::mat3(1.0f); + glm::mat3 m_InverseInertiaTensor = glm::mat3(1.0f); + float m_Restitution = 0.5f; + float m_Friction = 0.5f; + + // Collision + std::shared_ptr m_Collider; + + // Flags + bool m_IsKinematic = false; + bool m_UseGravity = true; + + // Update inertia tensor based on mass and collider + void UpdateInertiaTensor(); + + friend class PhysicsSystem; +}; + +struct CollisionInfo { + std::shared_ptr bodyA; + std::shared_ptr bodyB; + glm::vec3 contactPoint; + glm::vec3 normal; + float penetrationDepth; +}; + +class PhysicsSystem { +public: + PhysicsSystem(); + ~PhysicsSystem(); + + void Initialize(); + void Shutdown(); + + // Update physics simulation + void Update(float deltaTime); + + // RigidBody management + std::shared_ptr CreateRigidBody(); + void DestroyRigidBody(std::shared_ptr body); + + // World settings + void SetGravity(const glm::vec3& gravity) { m_Gravity = gravity; } + const glm::vec3& GetGravity() const { return m_Gravity; } + + // Collision detection + bool Raycast(const glm::vec3& origin, const glm::vec3& direction, float maxDistance, RaycastHit& hit); + +private: + std::vector> m_RigidBodies; + glm::vec3 m_Gravity = glm::vec3(0.0f, -9.81f, 0.0f); + + // Simulation steps + void IntegrateForces(RigidBody& body, float deltaTime); + void IntegrateVelocities(RigidBody& body, float deltaTime); + + // Collision detection and response + void DetectCollisions(std::vector& collisions); + void ResolveCollisions(std::vector& collisions); + + // Helper functions for collision detection + bool CheckCollision(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info); + bool SphereVsSphere(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info); + bool BoxVsBox(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info); + bool SphereVsBox(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info); +}; + +struct RaycastHit { + std::shared_ptr body; + glm::vec3 point; + glm::vec3 normal; + float distance; +}; + +} // namespace Physics +} // namespace Engine +---- + +This basic structure provides a foundation for simulating rigid body physics with collision detection and response. In a real implementation, you would likely use a physics library like Bullet, PhysX, or Havok for more advanced features and optimizations. + +=== Integrating with the Engine + +To integrate our physics system with the rest of our engine, we'll add it to our engine's main class: + +[source,cpp] +---- +// Engine.h +#include "Physics.h" + +namespace Engine { + +class Engine { +public: + // ... existing engine code ... + + Physics::PhysicsSystem& GetPhysicsSystem() { return m_PhysicsSystem; } + +private: + // ... existing engine members ... + + Physics::PhysicsSystem m_PhysicsSystem; +}; + +} // namespace Engine +---- + +And we'll initialize it during engine startup: + +[source,cpp] +---- +// Engine.cpp +void Engine::Initialize() { + // ... existing initialization code ... + + m_PhysicsSystem.Initialize(); +} + +void Engine::Shutdown() { + m_PhysicsSystem.Shutdown(); + + // ... existing shutdown code ... +} +---- + +=== Basic Implementation of Physics Simulation + +To keep the update loop easy to follow, think of a fixed‑timestep frame as six steps: + +1) Accumulate forces (e.g., gravity, user forces) +2) Integrate forces (update velocities with damping) +3) Detect collisions (broad/narrow checks per pair) +4) Resolve collisions (impulses + positional correction) +5) Integrate velocities (update positions and orientations) +6) Clear forces (prepare for next step) + +Let's implement the core physics simulation functions: + +[source,cpp] +---- +// Physics.cpp +#include "Physics.h" + +namespace Engine { +namespace Physics { + +void PhysicsSystem::Update(float deltaTime) { + // Fixed timestep for stability + const float fixedTimeStep = 1.0f / 60.0f; + + // Accumulate forces (e.g., gravity) + for (auto& body : m_RigidBodies) { + if (!body->IsKinematic() && body->IsGravityEnabled()) { + body->m_AccumulatedForce += m_Gravity * body->m_Mass; + } + } + + // Integrate forces + for (auto& body : m_RigidBodies) { + if (!body->IsKinematic()) { + IntegrateForces(*body, fixedTimeStep); + } + } + + // Detect and resolve collisions + std::vector collisions; + DetectCollisions(collisions); + ResolveCollisions(collisions); + + // Integrate velocities + for (auto& body : m_RigidBodies) { + if (!body->IsKinematic()) { + IntegrateVelocities(*body, fixedTimeStep); + } + } + + // Clear accumulated forces + for (auto& body : m_RigidBodies) { + body->m_AccumulatedForce = glm::vec3(0.0f); + body->m_AccumulatedTorque = glm::vec3(0.0f); + } +} + +void PhysicsSystem::IntegrateForces(RigidBody& body, float deltaTime) { + // Update linear velocity + body.m_LinearVelocity += (body.m_AccumulatedForce * body.m_InverseMass) * deltaTime; + + // Update angular velocity + body.m_AngularVelocity += glm::vec3(body.m_InverseInertiaTensor * glm::vec4(body.m_AccumulatedTorque, 0.0f)) * deltaTime; + + // Apply damping + const float linearDamping = 0.01f; + const float angularDamping = 0.01f; + body.m_LinearVelocity *= (1.0f - linearDamping); + body.m_AngularVelocity *= (1.0f - angularDamping); +} + +void PhysicsSystem::IntegrateVelocities(RigidBody& body, float deltaTime) { + // Update position + body.m_Position += body.m_LinearVelocity * deltaTime; + + // Update rotation + glm::quat angularVelocityQuat(0.0f, body.m_AngularVelocity.x, body.m_AngularVelocity.y, body.m_AngularVelocity.z); + body.m_Rotation += (angularVelocityQuat * body.m_Rotation) * 0.5f * deltaTime; + body.m_Rotation = glm::normalize(body.m_Rotation); +} + +void PhysicsSystem::DetectCollisions(std::vector& collisions) { + // Simple O(n²) collision detection + for (size_t i = 0; i < m_RigidBodies.size(); i++) { + for (size_t j = i + 1; j < m_RigidBodies.size(); j++) { + auto& bodyA = m_RigidBodies[i]; + auto& bodyB = m_RigidBodies[j]; + + // Skip if both bodies are kinematic + if (bodyA->IsKinematic() && bodyB->IsKinematic()) { + continue; + } + + // Skip if either body doesn't have a collider + if (!bodyA->GetCollider() || !bodyB->GetCollider()) { + continue; + } + + CollisionInfo info; + if (CheckCollision(*bodyA, *bodyB, info)) { + info.bodyA = bodyA; + info.bodyB = bodyB; + collisions.push_back(info); + } + } + } +} + +void PhysicsSystem::ResolveCollisions(std::vector& collisions) { + for (auto& collision : collisions) { + auto bodyA = collision.bodyA; + auto bodyB = collision.bodyB; + + // Calculate relative velocity + glm::vec3 relativeVelocity = bodyB->m_LinearVelocity - bodyA->m_LinearVelocity; + + // Calculate impulse magnitude + float velocityAlongNormal = glm::dot(relativeVelocity, collision.normal); + + // Don't resolve if velocities are separating + if (velocityAlongNormal > 0) { + continue; + } + + // Calculate restitution (bounciness) + float restitution = std::min(bodyA->m_Restitution, bodyB->m_Restitution); + + // Calculate impulse scalar + float j = -(1.0f + restitution) * velocityAlongNormal; + j /= bodyA->m_InverseMass + bodyB->m_InverseMass; + + // Apply impulse + glm::vec3 impulse = collision.normal * j; + + if (!bodyA->IsKinematic()) { + bodyA->m_LinearVelocity -= impulse * bodyA->m_InverseMass; + } + + if (!bodyB->IsKinematic()) { + bodyB->m_LinearVelocity += impulse * bodyB->m_InverseMass; + } + + // Resolve penetration (position correction) + const float percent = 0.2f; // usually 20% to 80% + const float slop = 0.01f; // small penetration allowed + glm::vec3 correction = std::max(collision.penetrationDepth - slop, 0.0f) * percent * collision.normal / (bodyA->m_InverseMass + bodyB->m_InverseMass); + + if (!bodyA->IsKinematic()) { + bodyA->m_Position -= correction * bodyA->m_InverseMass; + } + + if (!bodyB->IsKinematic()) { + bodyB->m_Position += correction * bodyB->m_InverseMass; + } + } +} + +bool PhysicsSystem::CheckCollision(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info) { + auto colliderA = bodyA.GetCollider(); + auto colliderB = bodyB.GetCollider(); + + if (colliderA->GetType() == ColliderType::Sphere && colliderB->GetType() == ColliderType::Sphere) { + return SphereVsSphere(bodyA, bodyB, info); + } + else if (colliderA->GetType() == ColliderType::Box && colliderB->GetType() == ColliderType::Box) { + return BoxVsBox(bodyA, bodyB, info); + } + else if (colliderA->GetType() == ColliderType::Sphere && colliderB->GetType() == ColliderType::Box) { + return SphereVsBox(bodyA, bodyB, info); + } + else if (colliderA->GetType() == ColliderType::Box && colliderB->GetType() == ColliderType::Sphere) { + bool result = SphereVsBox(bodyB, bodyA, info); + if (result) { + // Flip normal direction + info.normal = -info.normal; + } + return result; + } + + // Unsupported collision types + return false; +} + +bool PhysicsSystem::SphereVsSphere(const RigidBody& bodyA, const RigidBody& bodyB, CollisionInfo& info) { + auto sphereA = std::static_pointer_cast(bodyA.GetCollider()); + auto sphereB = std::static_pointer_cast(bodyB.GetCollider()); + + glm::vec3 posA = bodyA.GetPosition() + sphereA->GetOffset(); + glm::vec3 posB = bodyB.GetPosition() + sphereB->GetOffset(); + + float radiusA = sphereA->GetRadius(); + float radiusB = sphereB->GetRadius(); + + glm::vec3 direction = posB - posA; + float distance = glm::length(direction); + float minDistance = radiusA + radiusB; + + if (distance >= minDistance) { + return false; + } + + // Normalize direction + direction = distance > 0.0001f ? direction / distance : glm::vec3(0, 1, 0); + + info.contactPoint = posA + direction * radiusA; + info.normal = direction; + info.penetrationDepth = minDistance - distance; + + return true; +} + +// Implementation of BoxVsBox and SphereVsBox collision detection would go here +// These are more complex and would require additional helper functions + +} // namespace Physics +} // namespace Engine +---- + +=== Basic Usage Example + +Here's how you might use this physics system in a game: + +[source,cpp] +---- +// Game code +void Game::Initialize() { + // Create a ground plane + auto ground = m_Engine.GetPhysicsSystem().CreateRigidBody(); + ground->SetPosition(glm::vec3(0.0f, -1.0f, 0.0f)); + ground->SetKinematic(true); // Static object + auto groundCollider = std::make_shared(glm::vec3(50.0f, 1.0f, 50.0f)); + ground->SetCollider(groundCollider); + + // Create a dynamic box + auto box = m_Engine.GetPhysicsSystem().CreateRigidBody(); + box->SetPosition(glm::vec3(0.0f, 5.0f, 0.0f)); + box->SetMass(1.0f); + auto boxCollider = std::make_shared(glm::vec3(0.5f, 0.5f, 0.5f)); + box->SetCollider(boxCollider); + + // Create a dynamic sphere + auto sphere = m_Engine.GetPhysicsSystem().CreateRigidBody(); + sphere->SetPosition(glm::vec3(1.0f, 10.0f, 0.0f)); + sphere->SetMass(2.0f); + auto sphereCollider = std::make_shared(0.7f); + sphere->SetCollider(sphereCollider); + + // Store references to our objects + m_PhysicsObjects.push_back(ground); + m_PhysicsObjects.push_back(box); + m_PhysicsObjects.push_back(sphere); +} + +void Game::Update(float deltaTime) { + // Update physics + m_Engine.GetPhysicsSystem().Update(deltaTime); + + // Update visual representations of physics objects + for (auto& physicsObject : m_PhysicsObjects) { + auto visualObject = m_PhysicsToVisualMap[physicsObject]; + if (visualObject) { + visualObject->SetPosition(physicsObject->GetPosition()); + visualObject->SetRotation(physicsObject->GetRotation()); + } + } +} + +void Game::OnExplosion(const glm::vec3& position, float force) { + // Apply radial impulse to nearby objects + for (auto& physicsObject : m_PhysicsObjects) { + if (!physicsObject->IsKinematic()) { + glm::vec3 direction = physicsObject->GetPosition() - position; + float distance = glm::length(direction); + + if (distance < 10.0f) { + direction = glm::normalize(direction); + float impulseMagnitude = force * (1.0f - distance / 10.0f); + physicsObject->ApplyImpulse(direction * impulseMagnitude); + } + } + } +} +---- + +=== Limitations of Basic Physics Systems + +While this basic physics system provides the essential functionality for simulating rigid bodies in a game, it has several limitations: + +1. *Performance*: The O(n²) collision detection becomes a bottleneck with many objects. +2. *Limited Collision Shapes*: We've only implemented basic shapes like boxes and spheres. +3. *Stability Issues*: Simple integrators and collision resolution can lead to instability. +4. *No Continuous Collision Detection*: Fast-moving objects might tunnel through thin obstacles. +5. *Limited Constraints*: We haven't implemented joints, springs, or other constraints. +6. *CPU-Bound Processing*: All calculations are performed on the CPU, limiting scalability. + +In the next section, we'll explore how Vulkan compute shaders can address these limitations by offloading physics calculations to the GPU, particularly for large-scale simulations with many objects. + +link:03_vulkan_audio.adoc[Previous: Vulkan for Audio Processing] | link:05_vulkan_physics.adoc[Next: Vulkan for Physics Simulation] diff --git a/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc b/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc new file mode 100644 index 00000000..6157f080 --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/05_vulkan_physics.adoc @@ -0,0 +1,776 @@ +:pp: {plus}{plus} + += Subsystems: Vulkan for Physics Simulation + +== Enhancing Physics with Vulkan + +In the previous section, we implemented a basic physics system for our engine. Now, we'll explore how Vulkan's compute capabilities can enhance physics simulations, particularly for large-scale scenarios with many interacting objects. + +=== Why Use Vulkan for Physics? + +Traditional physics simulations are performed on the CPU, but there are several compelling reasons to leverage Vulkan compute shaders for physics calculations: + +1. *Parallelism*: Physics calculations for multiple objects can be performed in parallel, making them well-suited for GPU computation. +2. *Scalability*: GPU-based physics can handle thousands or even millions of objects with relatively little performance degradation. +3. *Reduced CPU Load*: Offloading physics to the GPU frees up CPU resources for game logic, AI, and other tasks. +4. *Unified Memory*: With Vulkan, we can share memory between physics and graphics, reducing data transfer overhead. +5. *Specialized Hardware*: Modern GPUs often include hardware features specifically designed to accelerate physics-like calculations. + +=== Common GPU Physics Applications + +While not all physics calculations are suitable for GPU acceleration, several common physics tasks can benefit significantly: + +1. *Particle Systems*: Simulating thousands of particles for effects like smoke, fire, or fluid. +2. *Cloth Simulation*: Calculating the behavior of cloth, hair, or other deformable objects. +3. *Soft Body Physics*: Simulating objects that can bend, stretch, or compress. +4. *Broad-Phase Collision Detection*: Quickly identifying potential collision pairs among many objects. +5. *Rigid Body Dynamics*: Simulating the movement of large numbers of rigid bodies. + +Let's focus on implementing GPU-accelerated rigid body dynamics and collision detection using Vulkan compute shaders. + +=== GPU-Accelerated Rigid Body Physics + +To implement GPU-accelerated physics, we'll need to: + +1. Store physics data in GPU-accessible buffers +2. Create compute shaders to perform physics calculations +3. Integrate the GPU physics with our existing CPU-based system + +Let's extend our physics system to include Vulkan-accelerated components. We’ll approach it in four steps: + +1) Step 1: Data layout (GPUPhysicsData/GPUCollisionData structures) +2) Step 2: GPU resource setup (descriptor set layout, pipelines, storage buffers, descriptor sets) +3) Step 3: Simulation dispatch (integrate → broad‑phase → narrow‑phase → resolve with pipeline barriers) +4) Step 4: Synchronization and readback (update GPU buffers, submit, read back state, integrate in Update) + +[NOTE] +==== +We avoid repeating Vulkan compute fundamentals here; focus stays on physics‑specific wiring. Use earlier chapters (link:../Engine_Architecture/04_resource_management.adoc[Resource Management], link:../Engine_Architecture/05_rendering_pipeline.adoc[Rendering Pipeline]) or the Vulkan Guide (https://docs.vulkan.org/guide/latest/) if you need a refresher on descriptors, buffers, or pipeline creation. +==== + +[source,cpp] +---- +// Physics.h (additions) +#include + +namespace Engine { +namespace Physics { + +// 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) +}; + +// 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 +}; + +// Extended PhysicsSystem with Vulkan acceleration +class PhysicsSystem { +public: + // ... existing methods ... + + // Enable/disable GPU acceleration + void SetGPUAccelerationEnabled(bool enabled) { m_GPUAccelerationEnabled = enabled; } + bool IsGPUAccelerationEnabled() const { return m_GPUAccelerationEnabled; } + + // Set the maximum number of objects that can be simulated on the GPU + void SetMaxGPUObjects(uint32_t maxObjects); + +private: + // ... existing members ... + + // GPU acceleration + bool m_GPUAccelerationEnabled = false; + uint32_t m_MaxGPUObjects = 1024; + uint32_t m_MaxGPUCollisions = 4096; + + // 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; + + // Command buffer for compute operations + vk::raii::CommandPool commandPool = nullptr; + vk::raii::CommandBuffer commandBuffer = nullptr; + }; + + VulkanResources m_VulkanResources; + + // Initialize Vulkan resources for physics simulation + void InitializeVulkanResources(); + void CleanupVulkanResources(); + + // Update physics data on the GPU + void UpdateGPUPhysicsData(); + + // Read back physics data from the GPU + void ReadbackGPUPhysicsData(); + + // Perform GPU-accelerated physics simulation + void SimulatePhysicsOnGPU(float deltaTime); +}; + +} // namespace Physics +} // namespace Engine +---- + +Now, let's implement the Vulkan-based physics simulation: + +[source,cpp] +---- +// Physics.cpp (implementation) + +void PhysicsSystem::InitializeVulkanResources() { + // Get Vulkan device from the engine + auto& device = m_Engine.GetVulkanDevice(); + + // Create compute shader modules + auto integrateShaderCode = LoadShaderFile("shaders/physics_integrate.comp.spv"); + vk::ShaderModuleCreateInfo integrateShaderModuleCreateInfo({}, integrateShaderCode.size() * sizeof(uint32_t), + reinterpret_cast(integrateShaderCode.data())); + m_VulkanResources.integrateShaderModule = vk::raii::ShaderModule(device, integrateShaderModuleCreateInfo); + + auto broadPhaseShaderCode = LoadShaderFile("shaders/physics_broad_phase.comp.spv"); + vk::ShaderModuleCreateInfo broadPhaseShaderModuleCreateInfo({}, broadPhaseShaderCode.size() * sizeof(uint32_t), + reinterpret_cast(broadPhaseShaderCode.data())); + m_VulkanResources.broadPhaseShaderModule = vk::raii::ShaderModule(device, broadPhaseShaderModuleCreateInfo); + + auto narrowPhaseShaderCode = LoadShaderFile("shaders/physics_narrow_phase.comp.spv"); + vk::ShaderModuleCreateInfo narrowPhaseShaderModuleCreateInfo({}, narrowPhaseShaderCode.size() * sizeof(uint32_t), + reinterpret_cast(narrowPhaseShaderCode.data())); + m_VulkanResources.narrowPhaseShaderModule = vk::raii::ShaderModule(device, narrowPhaseShaderModuleCreateInfo); + + auto resolveShaderCode = LoadShaderFile("shaders/physics_resolve.comp.spv"); + vk::ShaderModuleCreateInfo resolveShaderModuleCreateInfo({}, resolveShaderCode.size() * sizeof(uint32_t), + reinterpret_cast(resolveShaderCode.data())); + m_VulkanResources.resolveShaderModule = vk::raii::ShaderModule(device, resolveShaderModuleCreateInfo); + + // Create descriptor set layout + std::array bindings = { + // Physics data buffer + vk::DescriptorSetLayoutBinding(0, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // Collision data buffer + vk::DescriptorSetLayoutBinding(1, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // Pair buffer (for broad phase) + vk::DescriptorSetLayoutBinding(2, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute), + // Counter buffer + vk::DescriptorSetLayoutBinding(3, vk::DescriptorType::eStorageBuffer, 1, + vk::ShaderStageFlagBits::eCompute) + }; + + vk::DescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo({}, bindings); + m_VulkanResources.descriptorSetLayout = vk::raii::DescriptorSetLayout(device, descriptorSetLayoutCreateInfo); + + // Create pipeline layout + vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo({}, *m_VulkanResources.descriptorSetLayout); + m_VulkanResources.pipelineLayout = vk::raii::PipelineLayout(device, pipelineLayoutCreateInfo); + + // Create compute pipelines + vk::PipelineShaderStageCreateInfo integrateShaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.integrateShaderModule, "main"); + vk::ComputePipelineCreateInfo integrateComputePipelineCreateInfo({}, integrateShaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.integratePipeline = vk::raii::Pipeline(device, nullptr, integrateComputePipelineCreateInfo); + + vk::PipelineShaderStageCreateInfo broadPhaseShaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.broadPhaseShaderModule, "main"); + vk::ComputePipelineCreateInfo broadPhaseComputePipelineCreateInfo({}, broadPhaseShaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.broadPhasePipeline = vk::raii::Pipeline(device, nullptr, broadPhaseComputePipelineCreateInfo); + + vk::PipelineShaderStageCreateInfo narrowPhaseShaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.narrowPhaseShaderModule, "main"); + vk::ComputePipelineCreateInfo narrowPhaseComputePipelineCreateInfo({}, narrowPhaseShaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.narrowPhasePipeline = vk::raii::Pipeline(device, nullptr, narrowPhaseComputePipelineCreateInfo); + + vk::PipelineShaderStageCreateInfo resolveShaderStageCreateInfo({}, vk::ShaderStageFlagBits::eCompute, + *m_VulkanResources.resolveShaderModule, "main"); + vk::ComputePipelineCreateInfo resolveComputePipelineCreateInfo({}, resolveShaderStageCreateInfo, + *m_VulkanResources.pipelineLayout); + m_VulkanResources.resolvePipeline = vk::raii::Pipeline(device, nullptr, resolveComputePipelineCreateInfo); + + // Create descriptor pool + std::array poolSizes = { + vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 4) + }; + vk::DescriptorPoolCreateInfo descriptorPoolCreateInfo({}, 1, poolSizes); + m_VulkanResources.descriptorPool = vk::raii::DescriptorPool(device, descriptorPoolCreateInfo); + + // Allocate descriptor sets + vk::DescriptorSetAllocateInfo descriptorSetAllocateInfo(*m_VulkanResources.descriptorPool, + 1, &*m_VulkanResources.descriptorSetLayout); + m_VulkanResources.descriptorSets = vk::raii::DescriptorSets(device, descriptorSetAllocateInfo); + + // Create buffers for physics data + CreateBuffer(device, sizeof(GPUPhysicsData) * m_MaxGPUObjects, + vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.physicsBuffer, m_VulkanResources.physicsBufferMemory); + + CreateBuffer(device, sizeof(GPUCollisionData) * m_MaxGPUCollisions, + vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.collisionBuffer, m_VulkanResources.collisionBufferMemory); + + CreateBuffer(device, sizeof(uint32_t) * 2 * m_MaxGPUCollisions, + vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.pairBuffer, m_VulkanResources.pairBufferMemory); + + CreateBuffer(device, sizeof(uint32_t) * 2, + vk::BufferUsageFlagBits::eStorageBuffer, + m_VulkanResources.counterBuffer, m_VulkanResources.counterBufferMemory); + + // Update descriptor sets + std::array bufferInfos = { + vk::DescriptorBufferInfo(*m_VulkanResources.physicsBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.collisionBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.pairBuffer, 0, VK_WHOLE_SIZE), + vk::DescriptorBufferInfo(*m_VulkanResources.counterBuffer, 0, VK_WHOLE_SIZE) + }; + + std::array descriptorWrites = { + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 0, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[0]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 1, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[1]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 2, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[2]), + vk::WriteDescriptorSet(*m_VulkanResources.descriptorSets[0], 3, 0, 1, + vk::DescriptorType::eStorageBuffer, nullptr, &bufferInfos[3]) + }; + + device.updateDescriptorSets(descriptorWrites, {}); + + // Create command pool and command buffer + vk::CommandPoolCreateInfo commandPoolCreateInfo({}, m_Engine.GetVulkanQueueFamilyIndex()); + m_VulkanResources.commandPool = vk::raii::CommandPool(device, commandPoolCreateInfo); + + vk::CommandBufferAllocateInfo commandBufferAllocateInfo(*m_VulkanResources.commandPool, + vk::CommandBufferLevel::ePrimary, 1); + auto commandBuffers = vk::raii::CommandBuffers(device, commandBufferAllocateInfo); + m_VulkanResources.commandBuffer = std::move(commandBuffers[0]); + + // Initialize counter buffer + uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count + void* data; + vkMapMemory(device, *m_VulkanResources.counterBufferMemory, 0, sizeof(initialCounters), 0, &data); + memcpy(data, initialCounters, sizeof(initialCounters)); + vkUnmapMemory(device, *m_VulkanResources.counterBufferMemory); +} + +void PhysicsSystem::UpdateGPUPhysicsData() { + auto& device = m_Engine.GetVulkanDevice(); + + // Map the physics buffer + void* data; + vkMapMemory(device, *m_VulkanResources.physicsBufferMemory, 0, + sizeof(GPUPhysicsData) * m_RigidBodies.size(), 0, &data); + + // Copy physics data to the buffer + GPUPhysicsData* gpuData = static_cast(data); + for (size_t i = 0; i < m_RigidBodies.size(); i++) { + auto& body = m_RigidBodies[i]; + + gpuData[i].position = glm::vec4(body->GetPosition(), body->GetInverseMass()); + gpuData[i].rotation = glm::vec4(body->GetRotation().x, body->GetRotation().y, + body->GetRotation().z, body->GetRotation().w); + gpuData[i].linearVelocity = glm::vec4(body->GetLinearVelocity(), body->GetRestitution()); + gpuData[i].angularVelocity = glm::vec4(body->GetAngularVelocity(), body->GetFriction()); + gpuData[i].force = glm::vec4(body->m_AccumulatedForce, body->IsKinematic() ? 1.0f : 0.0f); + gpuData[i].torque = glm::vec4(body->m_AccumulatedTorque, body->IsGravityEnabled() ? 1.0f : 0.0f); + + // Set collider data based on collider type + auto collider = body->GetCollider(); + if (collider) { + switch (collider->GetType()) { + case ColliderType::Sphere: { + auto sphereCollider = std::static_pointer_cast(collider); + gpuData[i].colliderData = glm::vec4(sphereCollider->GetRadius(), 0.0f, 0.0f, + static_cast(ColliderType::Sphere)); + gpuData[i].colliderData2 = glm::vec4(collider->GetOffset(), 0.0f); + break; + } + case ColliderType::Box: { + auto boxCollider = std::static_pointer_cast(collider); + gpuData[i].colliderData = glm::vec4(boxCollider->GetHalfExtents(), + static_cast(ColliderType::Box)); + gpuData[i].colliderData2 = glm::vec4(collider->GetOffset(), 0.0f); + break; + } + default: + // Unsupported collider type + gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); + gpuData[i].colliderData2 = glm::vec4(0.0f); + break; + } + } else { + // No collider + gpuData[i].colliderData = glm::vec4(0.0f, 0.0f, 0.0f, -1.0f); + gpuData[i].colliderData2 = glm::vec4(0.0f); + } + } + + vkUnmapMemory(device, *m_VulkanResources.physicsBufferMemory); + + // Reset counters + uint32_t initialCounters[2] = { 0, 0 }; // [0] = pair count, [1] = collision count + vkMapMemory(device, *m_VulkanResources.counterBufferMemory, 0, sizeof(initialCounters), 0, &data); + memcpy(data, initialCounters, sizeof(initialCounters)); + vkUnmapMemory(device, *m_VulkanResources.counterBufferMemory); +} + +void PhysicsSystem::ReadbackGPUPhysicsData() { + auto& device = m_Engine.GetVulkanDevice(); + + // Map the physics buffer + void* data; + vkMapMemory(device, *m_VulkanResources.physicsBufferMemory, 0, + sizeof(GPUPhysicsData) * m_RigidBodies.size(), 0, &data); + + // Copy physics data from the buffer + GPUPhysicsData* gpuData = static_cast(data); + for (size_t i = 0; i < m_RigidBodies.size(); i++) { + auto& body = m_RigidBodies[i]; + + // Skip kinematic bodies + if (body->IsKinematic()) { + continue; + } + + body->SetPosition(glm::vec3(gpuData[i].position)); + body->SetRotation(glm::quat(gpuData[i].rotation.w, gpuData[i].rotation.x, + gpuData[i].rotation.y, gpuData[i].rotation.z)); + body->SetLinearVelocity(glm::vec3(gpuData[i].linearVelocity)); + body->SetAngularVelocity(glm::vec3(gpuData[i].angularVelocity)); + } + + vkUnmapMemory(device, *m_VulkanResources.physicsBufferMemory); +} + +void PhysicsSystem::SimulatePhysicsOnGPU(float deltaTime) { + auto& device = m_Engine.GetVulkanDevice(); + auto& queue = m_Engine.GetVulkanComputeQueue(); + + // Update physics data on the GPU + UpdateGPUPhysicsData(); + + // Record command buffer + vk::CommandBufferBeginInfo beginInfo(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); + m_VulkanResources.commandBuffer.begin(beginInfo); + + // Bind descriptor set + m_VulkanResources.commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.pipelineLayout, 0, + *m_VulkanResources.descriptorSets[0], {}); + + // Push constants for simulation parameters + struct { + float deltaTime; + float gravity[3]; + uint32_t numBodies; + } pushConstants; + + pushConstants.deltaTime = deltaTime; + pushConstants.gravity[0] = m_Gravity.x; + pushConstants.gravity[1] = m_Gravity.y; + pushConstants.gravity[2] = m_Gravity.z; + pushConstants.numBodies = static_cast(m_RigidBodies.size()); + + m_VulkanResources.commandBuffer.pushConstants(*m_VulkanResources.pipelineLayout, + vk::ShaderStageFlagBits::eCompute, 0, + sizeof(pushConstants), &pushConstants); + + // Step 1: Integrate forces and velocities + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.integratePipeline); + m_VulkanResources.commandBuffer.dispatch((pushConstants.numBodies + 63) / 64, 1, 1); + + // Memory barrier to ensure integration is complete before collision detection + vk::MemoryBarrier memoryBarrier(vk::AccessFlagBits::eShaderWrite, vk::AccessFlagBits::eShaderRead); + m_VulkanResources.commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, memoryBarrier, {}, {}); + + // Step 2: Broad-phase collision detection + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.broadPhasePipeline); + // Each thread checks one pair of objects + uint32_t numPairs = (pushConstants.numBodies * (pushConstants.numBodies - 1)) / 2; + m_VulkanResources.commandBuffer.dispatch((numPairs + 63) / 64, 1, 1); + + // Memory barrier to ensure broad phase is complete before narrow phase + m_VulkanResources.commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, memoryBarrier, {}, {}); + + // Step 3: Narrow-phase collision detection + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.narrowPhasePipeline); + // We don't know how many pairs were generated, so we use a conservative estimate + m_VulkanResources.commandBuffer.dispatch((m_MaxGPUCollisions + 63) / 64, 1, 1); + + // Memory barrier to ensure narrow phase is complete before resolution + m_VulkanResources.commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, + vk::PipelineStageFlagBits::eComputeShader, + {}, memoryBarrier, {}, {}); + + // Step 4: Collision resolution + m_VulkanResources.commandBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, + *m_VulkanResources.resolvePipeline); + // We don't know how many collisions were detected, so we use a conservative estimate + m_VulkanResources.commandBuffer.dispatch((m_MaxGPUCollisions + 63) / 64, 1, 1); + + m_VulkanResources.commandBuffer.end(); + + // Submit command buffer + vk::SubmitInfo submitInfo({}, {}, *m_VulkanResources.commandBuffer); + queue.submit(submitInfo, nullptr); + queue.waitIdle(); + + // Read back physics data from the GPU + ReadbackGPUPhysicsData(); +} + +void PhysicsSystem::Update(float deltaTime) { + if (m_GPUAccelerationEnabled && m_RigidBodies.size() <= m_MaxGPUObjects) { + // Use GPU-accelerated physics + SimulatePhysicsOnGPU(deltaTime); + } else { + // Fall back to CPU physics + // ... existing CPU physics code ... + } +} +---- + +=== Physics Compute Shaders + +Now, let's implement the compute shaders for our GPU-accelerated physics system: + +[source,glsl] +---- +// physics_integrate.comp +#version 450 + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +// Push constants +layout(push_constant) uniform PushConstants { + float deltaTime; + vec3 gravity; + uint numBodies; +} pushConstants; + +// Physics data +struct PhysicsData { + vec4 position; // xyz = position, w = inverse mass + vec4 rotation; // quaternion + vec4 linearVelocity; // xyz = velocity, w = restitution + vec4 angularVelocity; // xyz = angular velocity, w = friction + vec4 force; // xyz = force, w = is kinematic (0 or 1) + vec4 torque; // xyz = torque, w = use gravity (0 or 1) + vec4 colliderData; // type-specific data (e.g., radius for spheres) + vec4 colliderData2; // additional collider data (e.g., box half extents) +}; + +layout(std430, binding = 0) buffer PhysicsBuffer { + PhysicsData bodies[]; +} physicsBuffer; + +// Quaternion multiplication +vec4 quatMul(vec4 q1, vec4 q2) { + return vec4( + q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, + q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, + q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, + q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z + ); +} + +// Quaternion normalization +vec4 quatNormalize(vec4 q) { + float len = length(q); + if (len > 0.0001) { + return q / len; + } + return vec4(0, 0, 0, 1); +} + +void main() { + uint gID = gl_GlobalInvocationID.x; + + // Check if this invocation is within the number of bodies + if (gID >= pushConstants.numBodies) { + return; + } + + // Get physics data for this body + PhysicsData body = physicsBuffer.bodies[gID]; + + // Skip kinematic bodies + if (body.force.w > 0.5) { + return; + } + + // Apply gravity if enabled + if (body.torque.w > 0.5) { + body.force.xyz += pushConstants.gravity / body.position.w; + } + + // Integrate forces + body.linearVelocity.xyz += body.force.xyz * body.position.w * pushConstants.deltaTime; + body.angularVelocity.xyz += body.torque.xyz * pushConstants.deltaTime; // Simplified, should use inertia tensor + + // Apply damping + const float linearDamping = 0.01; + const float angularDamping = 0.01; + body.linearVelocity.xyz *= (1.0 - linearDamping); + body.angularVelocity.xyz *= (1.0 - angularDamping); + + // Integrate velocities + body.position.xyz += body.linearVelocity.xyz * pushConstants.deltaTime; + + // Update rotation + vec4 angularVelocityQuat = vec4(body.angularVelocity.xyz * 0.5, 0.0); + vec4 rotationDelta = quatMul(angularVelocityQuat, body.rotation); + body.rotation = quatNormalize(body.rotation + rotationDelta * pushConstants.deltaTime); + + // Write updated data back to buffer + physicsBuffer.bodies[gID] = body; +} +---- + +[source,glsl] +---- +// physics_broad_phase.comp +#version 450 + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +// Push constants +layout(push_constant) uniform PushConstants { + float deltaTime; + vec3 gravity; + uint numBodies; +} pushConstants; + +// Physics data +struct PhysicsData { + vec4 position; // xyz = position, w = inverse mass + vec4 rotation; // quaternion + vec4 linearVelocity; // xyz = velocity, w = restitution + vec4 angularVelocity; // xyz = angular velocity, w = friction + vec4 force; // xyz = force, w = is kinematic (0 or 1) + vec4 torque; // xyz = torque, w = use gravity (0 or 1) + vec4 colliderData; // type-specific data (e.g., radius for spheres) + vec4 colliderData2; // additional collider data (e.g., box half extents) +}; + +layout(std430, binding = 0) buffer PhysicsBuffer { + PhysicsData bodies[]; +} physicsBuffer; + +// Pair buffer for potential collisions +layout(std430, binding = 2) buffer PairBuffer { + uvec2 pairs[]; +} pairBuffer; + +// Counter buffer +layout(std430, binding = 3) buffer CounterBuffer { + uint pairCount; + uint collisionCount; +} counterBuffer; + +// Compute AABB for a body +void computeAABB(PhysicsData body, out vec3 min, out vec3 max) { + // Default to a small AABB + min = body.position.xyz - vec3(0.1); + max = body.position.xyz + vec3(0.1); + + // Check collider type + int colliderType = int(body.colliderData.w); + + if (colliderType == 0) { // Sphere + float radius = body.colliderData.x; + vec3 center = body.position.xyz + body.colliderData2.xyz; + min = center - vec3(radius); + max = center + vec3(radius); + } + else if (colliderType == 1) { // Box + vec3 halfExtents = body.colliderData.xyz; + vec3 center = body.position.xyz + body.colliderData2.xyz; + // This is simplified - should account for rotation + min = center - halfExtents; + max = center + halfExtents; + } +} + +bool aabbOverlap(vec3 minA, vec3 maxA, vec3 minB, vec3 maxB) { + return all(lessThan(minA, maxB)) && all(lessThan(minB, maxA)); +} + +void main() { + uint gID = gl_GlobalInvocationID.x; + + // Calculate which pair of bodies this thread should check + uint numBodies = pushConstants.numBodies; + uint numPairs = (numBodies * (numBodies - 1)) / 2; + + if (gID >= numPairs) { + return; + } + + // Convert linear index to pair indices (i, j) where i < j + uint i = 0; + uint j = 0; + + // This is a mathematical formula to convert a linear index to a pair of indices + uint row = uint(floor(sqrt(float(2 * gID + 0.25)) - 0.5)); + i = row; + j = gID - (row * (row + 1)) / 2; + + // Ensure j > i + j += i + 1; + + // Get physics data for both bodies + PhysicsData bodyA = physicsBuffer.bodies[i]; + PhysicsData bodyB = physicsBuffer.bodies[j]; + + // Skip if both bodies are kinematic + if (bodyA.force.w > 0.5 && bodyB.force.w > 0.5) { + return; + } + + // Skip if either body doesn't have a collider + if (bodyA.colliderData.w < 0 || bodyB.colliderData.w < 0) { + return; + } + + // Compute AABBs + vec3 minA, maxA, minB, maxB; + computeAABB(bodyA, minA, maxA); + computeAABB(bodyB, minB, maxB); + + // Check for AABB overlap + if (aabbOverlap(minA, maxA, minB, maxB)) { + // Add to potential collision pairs + uint pairIndex = atomicAdd(counterBuffer.pairCount, 1); + pairBuffer.pairs[pairIndex] = uvec2(i, j); + } +} +---- + +The narrow-phase and resolve shaders would follow a similar pattern, implementing the detailed collision detection and resolution algorithms. + +=== Performance Considerations + +When implementing GPU-accelerated physics with Vulkan, consider these performance optimizations: + +1. *Batch Processing*: Process multiple physics steps in a single dispatch to amortize the overhead of command submission. +2. *Memory Transfers*: Minimize transfers between CPU and GPU memory by keeping physics data on the GPU when possible. +3. *Spatial Partitioning*: Implement grid or tree-based spatial partitioning to reduce the number of potential collision pairs. +4. *Workgroup Size*: Tune the workgroup size based on your target hardware for optimal performance. +5. *Memory Layout*: Organize physics data for optimal cache coherency on the GPU. + +=== Integration with the Engine + +To integrate the GPU-accelerated physics into our engine, we need to modify the `PhysicsSystem::Initialize` method: + +[source,cpp] +---- +void PhysicsSystem::Initialize() { + // Initialize basic physics system + // ... + + // Initialize Vulkan resources for GPU-accelerated physics + if (m_Engine.IsVulkanInitialized()) { + InitializeVulkanResources(); + m_GPUAccelerationEnabled = true; + } +} + +void PhysicsSystem::Shutdown() { + // Cleanup Vulkan resources + if (m_Engine.IsVulkanInitialized()) { + CleanupVulkanResources(); + } + + // Shutdown basic physics system + // ... +} +---- + +=== Advantages of Vulkan-Based Physics + +By implementing physics simulation with Vulkan compute shaders, we gain several advantages: + +1. *Scalability*: The GPU can simulate thousands or even millions of objects in parallel. +2. *Performance*: GPU-accelerated physics can be orders of magnitude faster than CPU-based solutions for large-scale simulations. +3. *CPU Offloading*: Physics processing no longer competes with game logic for CPU resources. +4. *Advanced Simulations*: The GPU's computational power enables more complex physics simulations like fluid dynamics or cloth. + +=== Limitations and Considerations + +While Vulkan-based physics offers many advantages, there are some limitations to consider: + +1. *Complexity*: Implementing and debugging GPU-based physics is more complex than CPU-based solutions. +2. *Precision*: GPUs typically use single-precision floating-point, which may lead to numerical stability issues in some simulations. +3. *Platform Support*: Not all platforms support Vulkan, so you may need fallback CPU implementations. +4. *Synchronization*: Keeping CPU and GPU physics data in sync can be challenging and may introduce latency. + +=== Real-World Applications + +Several modern game engines and physics middleware solutions leverage GPU acceleration for physics simulations: + +1. *NVIDIA PhysX*: Supports GPU acceleration for certain physics calculations. +2. *Bullet Physics*: Has experimental GPU acceleration using compute shaders. +3. *Flex*: NVIDIA's particle-based physics solver designed specifically for GPU acceleration. +4. *Custom Solutions*: AAA game studios often implement custom GPU-accelerated physics for their titles. + +By implementing Vulkan-based physics in our engine, we're following industry best practices for high-performance physics in modern games. + +=== Conclusion + +In this chapter, we've explored how Vulkan compute shaders can be used to accelerate both audio and physics processing in a game engine. By leveraging the GPU's massive parallel processing capabilities, we can create more immersive and dynamic game worlds with realistic audio and physics simulations. + +The techniques we've covered demonstrate the versatility of Vulkan beyond traditional graphics rendering. As you continue to develop your engine, consider other areas where GPU acceleration might provide benefits, such as AI pathfinding, procedural generation, or particle systems. + +link:04_physics_basics.adoc[Previous: Physics Basics] | link:06_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc new file mode 100644 index 00000000..c130b31c --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/06_conclusion.adoc @@ -0,0 +1,111 @@ +:pp: {plus}{plus} + += Subsystems: Conclusion + +== Conclusion + +In this chapter, we've explored how to implement and enhance two critical engine subsystems—Audio and Physics—using Vulkan's compute capabilities. Let's summarize what we've learned and discuss potential future directions. + +=== What We've Learned + +==== Audio Subsystems + +We started by implementing a basic audio system that provides the foundation for sound playback in our engine. This system includes: + +* Audio resource management for loading and playing sound files +* Spatial audio positioning based on listener and source positions +* A flexible architecture that can be integrated with various audio backends + +We then enhanced this basic system with Vulkan compute shaders to implement Head-Related Transfer Function (HRTF) processing for more realistic 3D audio. This approach demonstrated: + +* How to offload computationally intensive audio processing to the GPU +* Techniques for implementing real-time convolution using compute shaders +* Methods for sharing data efficiently between CPU and GPU audio processing + +==== Physics Subsystems + +Similarly, we implemented a basic physics system that provides rigid body dynamics and collision detection. This system includes: + +* Rigid body simulation with forces, impulses, and collisions +* Various collider types for different geometric shapes +* Integration with the rest of our engine for visual representation of physics objects + +We then enhanced this system with Vulkan compute shaders to accelerate physics calculations, demonstrating: + +* Techniques for parallel physics simulation on the GPU +* Multi-stage physics processing (integration, broad phase, narrow phase, resolution) +* Methods for handling large numbers of physics objects efficiently + +==== Vulkan Integration + +Throughout both subsystems, we leveraged Vulkan's compute capabilities. We demonstrated: + +* Creating and managing compute pipelines for non-graphical tasks +* Efficient memory sharing between CPU and GPU +* Synchronization techniques for ensuring correct execution order +* Performance optimization strategies for compute shader workloads + +=== Potential Improvements + +While our implementations provide a solid foundation, there are several areas where they could be enhanced: + +==== Audio Improvements + +* *Advanced HRTF Models*: Implement more sophisticated HRTF models that account for individual differences in head and ear shapes. +* *Environmental Effects*: Add reverb, occlusion, and other environmental effects based on scene geometry. +* *Streaming Audio*: Implement streaming for large audio files to reduce memory usage. +* *Compression*: Add support for compressed audio formats to reduce memory and bandwidth requirements. +* *Voice Communication*: Integrate real-time voice processing for multiplayer games. + +==== Physics Improvements + +* *Advanced Collision Shapes*: Add support for more complex collision shapes like convex hulls and trimeshes. +* *Constraints and Joints*: Implement various types of constraints and joints for more complex mechanical systems. +* *Continuous Collision Detection*: Add support for detecting collisions between fast-moving objects. +* *Soft Body Physics*: Extend the system to support deformable objects like cloth, ropes, and soft bodies. +* *Fluid Simulation*: Implement fluid dynamics for realistic water, smoke, and fire effects. + +==== General Improvements + +* *Driver and Platform Coverage*: Test the subsystems across a representative set of Vulkan-capable platforms and drivers (e.g., Windows/Linux, major IHVs, Android, and macOS via MoltenVK). Non-Vulkan fallbacks are out of scope for this tutorial. +* *Profiling and Optimization*: Add detailed profiling to identify and address performance bottlenecks. +* *Memory Management*: Use allocator suballocation strategies (e.g., Vulkan Memory Allocator or custom pools), batch buffer/image allocations, and group resources by usage to reduce fragmentation and improve cache locality. +* *Multi-Threading*: Further optimize CPU-side processing with multi-threading where appropriate. + +=== Integration with Other Engine Systems + +As you continue developing your engine, consider how these subsystems interact with other components: + +* *Rendering System*: Visualize physics debug information, audio sources, and listener positions. +* *Animation System*: Synchronize animations with audio events and physics interactions. +* *Scripting System*: Provide high-level interfaces for controlling audio and physics from game scripts. +* *Networking*: Implement efficient synchronization of audio and physics state across networked clients. + +=== Real-World Considerations + +When using these subsystems in production applications, keep these considerations in mind: + +* *Performance Profiling*: Regularly profile your audio and physics systems to ensure they're not becoming bottlenecks. +* *Memory Usage*: Monitor memory usage, especially for large numbers of audio sources or physics objects. +* *Platform Differences*: Test on various hardware configurations to ensure consistent behavior. +* *Power Consumption*: Be mindful of power usage, especially on mobile devices where GPU compute can drain batteries quickly. + +=== Final Thoughts + +Audio and physics are essential components that contribute significantly to the immersion and interactivity of modern games. By leveraging Vulkan's compute capabilities, we can create more sophisticated and performant implementations of these subsystems, enabling richer and more dynamic game experiences. + +The techniques we've explored in this chapter demonstrate the versatility of Vulkan beyond traditional graphics rendering. As you continue to develop your engine, consider other areas where GPU acceleration might provide benefits, such as AI pathfinding, procedural generation, or particle systems. + +Remember that the implementations provided here are starting points. Real-world engines often require customization and optimization based on the specific needs of your games and target platforms. Don't hesitate to experiment and extend these systems to meet your unique requirements. + +=== Code Examples + +The complete code for this chapter can be found in the following files: + +* `simple_engine/30_audio_subsystem.cpp`: Implementation of the audio subsystem with Vulkan HRTF processing +* `simple_engine/31_physics_subsystem.cpp`: Implementation of the physics subsystem with Vulkan acceleration + +link:../../attachments/simple_engine/30_audio_subsystem.cpp[Audio Subsystem C{pp} code] +link:../../attachments/simple_engine/31_physics_subsystem.cpp[Physics Subsystem C{pp} code] + +xref:05_vulkan_physics.adoc[Previous: Vulkan for Physics Simulation] | xref:../Tooling/01_introduction.adoc[Next: Tooling] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Subsystems/index.adoc b/en/Building_a_Simple_Engine/Subsystems/index.adoc new file mode 100644 index 00000000..2b9c41e1 --- /dev/null +++ b/en/Building_a_Simple_Engine/Subsystems/index.adoc @@ -0,0 +1,14 @@ +:pp: {plus}{plus} + += Subsystems + +This chapter covers the implementation of critical engine subsystems - Audio and Physics - with a focus on leveraging Vulkan's compute capabilities for enhanced performance. + +* link:01_introduction.adoc[Introduction] +* link:02_audio_basics.adoc[Audio Basics] +* link:03_vulkan_audio.adoc[Vulkan for Audio Processing] +* link:04_physics_basics.adoc[Physics Basics] +* link:05_vulkan_physics.adoc[Vulkan for Physics Simulation] +* link:06_conclusion.adoc[Conclusion] + +link:../Loading_Models/09_conclusion.adoc[Previous: Loading Models Conclusion] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc b/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc new file mode 100644 index 00000000..4576d626 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/01_introduction.adoc @@ -0,0 +1,39 @@ +:pp: {plus}{plus} + += Tooling: Introduction + +== Introduction to Engine Tooling + +In previous chapters, we've built the foundation of our simple engine, implementing core components like the rendering pipeline, camera systems, model loading, and essential subsystems like audio and physics. Now, we're ready to explore the tooling ecosystem that supports the development, debugging, and distribution of a professional Vulkan application. + +Effective tooling is critical for maintaining productivity, ensuring quality, and delivering a robust final product. While these tools may seem separate from the engine itself, they are integral to the development process and can significantly impact the quality and maintainability of your code. + +=== What We'll Cover + +This chapter will equip you with the professional tooling ecosystem that transforms a working Vulkan application into a maintainable, debuggable, and deployable product. We'll begin by implementing a continuous integration and continuous deployment pipeline specifically designed for Vulkan's unique requirements. This foundation ensures that your application builds consistently across platforms while catching integration issues before they reach users. + +Debugging Vulkan applications presents unique challenges that traditional debugging approaches can't address effectively. We'll master both Vulkan's built-in debugging extensions like VK_KHR_debug_utils and external tools like RenderDoc, creating a comprehensive debugging workflow that can diagnose everything from validation layer warnings to complex rendering pipeline issues. + +Robust crash handling becomes crucial as your application moves toward production deployment. We'll implement systems that can gracefully handle unexpected failures, generate detailed minidumps for post-mortem analysis, and provide users with meaningful recovery options rather than abrupt terminations. + +Finally, we'll explore Vulkan extensions designed specifically for application robustness, such as VK_EXT_robustness2, which help your application handle edge cases and undefined behavior gracefully. These extensions transform potential crashes into recoverable situations, improving the overall user experience. + +=== Prerequisites + +This chapter assumes solid understanding of the Vulkan fundamentals and engine architecture we've built throughout the previous chapters. The tooling we'll implement needs to integrate with your existing systems—CI/CD pipelines must understand your project structure, debugging tools must work with your rendering pipeline, and crash handling must respect your engine's resource management patterns. + +Experience with modern C++ concepts becomes particularly important here, as professional tooling often leverages advanced language features for reliability and maintainability. C++17 and C++20 features like structured bindings, concepts, and coroutines appear frequently in production tooling code, and understanding these patterns will help you implement robust solutions. + +A basic familiarity with software development workflows and tools will provide context for the systems we'll build. While we'll explain the specific implementations, understanding why continuous integration matters, how debugging fits into development cycles, and why crash reporting improves user experience will help you appreciate the architectural decisions we make throughout this chapter. + +You should also be familiar with the following chapters from the main tutorial: + +* Basic Vulkan concepts: +** xref:../../03_Drawing_a_triangle/03_Drawing/01_Command_buffers.adoc[Command buffers] +** xref:../../03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.adoc[Graphics pipelines] +* xref:../../04_Vertex_buffers/00_Vertex_input_description.adoc[Vertex] and xref:../../04_Vertex_buffers/03_Index_buffer.adoc[index buffers] +* xref:../../05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.adoc[Uniform buffers] + +Let's begin by exploring how to set up a CI/CD pipeline for Vulkan projects. + +link:../Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | link:02_cicd.adoc[Next: CI/CD for Vulkan Projects] diff --git a/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc new file mode 100644 index 00000000..aac3d0b5 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/02_cicd.adoc @@ -0,0 +1,239 @@ +:pp: {plus}{plus} + += Tooling: CI/CD for Vulkan Projects + +== Continuous Integration and Deployment for Vulkan + +Continuous Integration (CI) and Continuous Deployment (CD) are essential practices in modern software development. They help ensure code quality, catch issues early, and streamline the release process. For Vulkan applications, which often need to run on multiple platforms with different GPU architectures, a robust CI/CD pipeline is particularly valuable. + +=== Setting Up a CI/CD Pipeline + +Let's explore how to set up a CI/CD pipeline specifically tailored for Vulkan projects. We'll use GitHub Actions as our example platform, but the concepts apply to other CI/CD systems like GitLab CI, Jenkins, or Azure DevOps. + +==== Basic Pipeline Structure + +A typical CI/CD pipeline for a Vulkan project might include these stages: + +1. *Build*: Compile the application on multiple platforms (Windows, Linux, macOS) +2. *Test*: Run unit tests and integration tests +3. *Package*: Create distributable packages for each platform +4. *Deploy*: Deploy to a staging environment or release to users + +Here's a basic GitHub Actions workflow file for a Vulkan project: + +[source,yaml] +---- +name: Vulkan CI/CD + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Vulkan SDK + uses: humbletim/install-vulkan-sdk@v1.1.1 + with: + version: latest + cache: true + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} + + - name: Build + run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}} + + - name: Test + working-directory: ${{github.workspace}}/build + run: ctest -C ${{matrix.build_type}} + + - name: Package + if: matrix.build_type == 'Release' + run: | + # Platform-specific packaging commands + if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then + # Linux packaging (e.g., .deb or .AppImage) + echo "Packaging for Linux" + elif [ "${{ matrix.os }}" == "windows-latest" ]; then + # Windows packaging (e.g., .exe installer) + echo "Packaging for Windows" + elif [ "${{ matrix.os }}" == "macos-latest" ]; then + # macOS packaging (e.g., .app bundle or .dmg) + echo "Packaging for macOS" + fi +---- + +==== Vulkan-Specific Considerations + +When setting up CI/CD for Vulkan projects, consider these specific challenges: + +===== Vulkan SDK Installation + +Ensure your CI environment has the Vulkan SDK installed. Many CI platforms don't include it by default. In the example above, we used a GitHub Action to install the SDK. + +===== GPU Availability in CI Environments + +Most CI environments don't have GPUs available, which can make testing Vulkan applications challenging. Consider these approaches: + +* Use software rendering (e.g., SwiftShader) for basic tests +* Implement a headless testing mode that doesn't require a display +* Use cloud-based GPU instances for more comprehensive testing + +===== Platform-Specific Vulkan Loaders + +Different platforms handle Vulkan loading differently. Ensure your build system correctly handles these differences: + +* Windows: Vulkan-1.dll is typically loaded at runtime +* Linux: libvulkan.so.1 is loaded at runtime +* macOS: MoltenVK provides Vulkan support via Metal + +===== Shader Compilation + +Shader compilation can be a complex part of the build process. Consider these approaches: + +* Pre-compile shaders during the build phase +* Include shader compilation in your CI pipeline to catch GLSL/SPIR-V errors early +* Use a shader management system that handles cross-platform differences + +=== Automating Testing for Vulkan Applications + +Testing Vulkan applications presents unique challenges. Here are some approaches to consider: + +==== Unit Testing Vulkan Code + +[source,cpp] +---- +import std; +import vulkan_raii; + +// A testable function using vk::raii +bool create_pipeline(vk::raii::Device& device, + vk::raii::RenderPass& render_pass, + vk::raii::PipelineLayout& layout, + vk::raii::Pipeline& out_pipeline) { + try { + // Pipeline creation code using RAII + return true; + } catch (vk::SystemError& err) { + std::cerr << "Failed to create pipeline: " << err.what() << std::endl; + return false; + } +} + +// In a test file +TEST_CASE("Pipeline creation") { + // Setup test environment with mock or real Vulkan objects + vk::raii::Context context; + auto instance = create_test_instance(context); + auto device = create_test_device(instance); + auto render_pass = create_test_render_pass(device); + auto layout = create_test_pipeline_layout(device); + + vk::raii::Pipeline pipeline{nullptr}; + REQUIRE(create_pipeline(device, render_pass, layout, pipeline)); + REQUIRE(pipeline); +} +---- + +==== Integration Testing + +For integration testing, consider creating a headless rendering mode that can run in CI environments: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class HeadlessRenderer { +public: + HeadlessRenderer() { + // Initialize Vulkan without surface + init_vulkan(); + } + + bool render_frame() { + // Render to an image without presenting + try { + // Rendering code + return true; + } catch (vk::SystemError& err) { + std::cerr << "Render failed: " << err.what() << std::endl; + return false; + } + } + + // Compare rendered image with reference + bool verify_output(const std::string& reference_image) { + // Image comparison code + return true; + } + +private: + void init_vulkan() { + // Vulkan initialization code + } + + vk::raii::Context context; + vk::raii::Instance instance{nullptr}; + vk::raii::PhysicalDevice physical_device{nullptr}; + vk::raii::Device device{nullptr}; + // Other Vulkan objects +}; + +// In a test file +TEST_CASE("Render output matches reference") { + HeadlessRenderer renderer; + REQUIRE(renderer.render_frame()); + REQUIRE(renderer.verify_output("reference_image.png")); +} +---- + +=== Distribution Considerations + +Once your application passes all tests, the final stage is packaging and distribution. Here are some considerations: + +==== Packaging Vulkan Applications + +* Include the appropriate Vulkan loader for each platform +* Package shader files or pre-compiled SPIR-V +* Consider using platform-specific packaging tools: + ** Windows: NSIS, WiX, or MSIX + ** Linux: AppImage, Flatpak, or .deb/.rpm packages + ** macOS: DMG or App Store packages + +==== Handling Vulkan Dependencies + +Ensure your package includes or correctly handles all dependencies: + +* Vulkan loader (or instructions to install it) +* Any required Vulkan extensions +* GPU driver requirements + +==== Versioning and Updates + +Implement a versioning system that includes: + +* Application version +* Minimum required Vulkan version +* Required extensions and their versions + +=== Conclusion + +A well-designed CI/CD pipeline is essential for maintaining quality and productivity when developing Vulkan applications. By automating building, testing, and packaging, you can focus more on developing features and less on manual processes. + +In the next section, we'll explore debugging tools for Vulkan applications, including the powerful VK_KHR_debug_utils extension and external tools like RenderDoc. + +link:01_introduction.adoc[Previous: Introduction] | link:03_debugging_and_renderdoc.adoc[Next: Debugging with VK_KHR_debug_utils and RenderDoc] diff --git a/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc new file mode 100644 index 00000000..8b7820d9 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/03_debugging_and_renderdoc.adoc @@ -0,0 +1,363 @@ +:pp: {plus}{plus} + += Tooling: Debugging with VK_KHR_debug_utils and RenderDoc + +== Debugging Vulkan Applications + +Debugging graphics applications can be challenging due to their complex, parallel nature and the fact that much of the processing happens on the GPU. Vulkan, with its explicit design, provides powerful debugging tools that can help identify and fix issues in your application. In this section, we'll explore two key approaches to debugging Vulkan applications: + +1. Using the VK_KHR_debug_utils extension for in-application debugging +2. Using external tools like RenderDoc for frame capture and analysis + +=== Using VK_KHR_debug_utils + +The VK_KHR_debug_utils extension provides a comprehensive set of tools for debugging Vulkan applications. It allows you to: + +* Label objects with meaningful names +* Mark the beginning and end of command buffer regions +* Insert debug markers +* Set up debug messengers to receive validation layer messages + +Let's explore how to use these features with C++20 modules and vk::raii. + +==== Setting Up Debug Messaging + +First, let's set up a debug messenger to receive validation layer messages: + +[source,cpp] +---- +import std; +import vulkan_raii; + +// Debug callback function +VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback( + VkDebugUtilsMessageSeverityFlagBitsEXT message_severity, + VkDebugUtilsMessageTypeFlagsEXT message_type, + const VkDebugUtilsMessengerCallbackDataEXT* callback_data, + void* user_data) { + + // Convert severity to string + std::string severity; + if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) { + severity = "VERBOSE"; + } else if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) { + severity = "INFO"; + } else if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + severity = "WARNING"; + } else if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + severity = "ERROR"; + } + + // Convert type to string + std::string type; + if (message_type & VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT) { + type = "GENERAL"; + } else if (message_type & VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT) { + type = "VALIDATION"; + } else if (message_type & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT) { + type = "PERFORMANCE"; + } + + // Log the message + std::cerr << "[" << severity << ": " << type << "] " + << callback_data->pMessage << std::endl; + + // Return false to indicate the Vulkan call should not be aborted + return VK_FALSE; +} + +// Create a debug messenger using vk::raii +vk::raii::DebugUtilsMessengerEXT create_debug_messenger(vk::raii::Instance& instance) { + vk::DebugUtilsMessengerCreateInfoEXT create_info{}; + create_info.setMessageSeverity( + vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError + ); + create_info.setMessageType( + vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance + ); + create_info.setPfnUserCallback(debug_callback); + + return vk::raii::DebugUtilsMessengerEXT(instance, create_info); +} +---- + +==== Object Naming + +One of the most useful features of VK_KHR_debug_utils is the ability to give meaningful names to Vulkan objects. This makes debugging much easier, as you can identify objects in validation layer messages and tools like RenderDoc: + +[source,cpp] +---- +// Helper function to set a name on any Vulkan handle +template +void set_object_name(vk::raii::Device& device, T handle, const std::string& name) { + vk::DebugUtilsObjectNameInfoEXT name_info{}; + name_info.setObjectType(get_object_type()); + name_info.setObjectHandle(reinterpret_cast(static_cast(handle))); + name_info.setPObjectName(name.c_str()); + + device.setDebugUtilsObjectNameEXT(name_info); +} + +// Example usage +void name_vulkan_objects(vk::raii::Device& device) { + // Name the device itself + set_object_name(device, *device, "Main Device"); + + // Name a buffer + vk::BufferCreateInfo buffer_info{}; + // ... set buffer creation parameters + vk::raii::Buffer buffer(device, buffer_info); + set_object_name(device, *buffer, "Vertex Buffer"); + + // Name a pipeline + vk::raii::Pipeline pipeline = create_graphics_pipeline(device); + set_object_name(device, *pipeline, "Main Render Pipeline"); +} +---- + +==== Command Buffer Labeling + +You can also label regions of command buffer execution, which helps identify where issues occur during rendering: + +[source,cpp] +---- +void record_command_buffer(vk::raii::CommandBuffer& cmd_buffer) { + cmd_buffer.begin({vk::CommandBufferUsageFlagBits::eOneTimeSubmit}); + + // Begin a labeled region + vk::DebugUtilsLabelEXT label_info{}; + label_info.setPLabelName("Shadow Pass"); + label_info.setColor(std::array{0.0f, 0.0f, 0.0f, 1.0f}); // Black for shadow pass + cmd_buffer.beginDebugUtilsLabelEXT(label_info); + + // Record shadow pass commands + // ... + + // End the labeled region + cmd_buffer.endDebugUtilsLabelEXT(); + + // Begin another labeled region + label_info.setPLabelName("Main Render Pass"); + label_info.setColor(std::array{0.0f, 1.0f, 0.0f, 1.0f}); // Green for main pass + cmd_buffer.beginDebugUtilsLabelEXT(label_info); + + // Record main render pass commands + // ... + + // Insert a marker within this region + cmd_buffer.insertDebugUtilsLabelEXT({ + "Drawing Opaque Objects", + std::array{1.0f, 1.0f, 1.0f, 1.0f} + }); + + // More rendering commands + // ... + + // End the labeled region + cmd_buffer.endDebugUtilsLabelEXT(); + + cmd_buffer.end(); +} +---- + +==== Queue Labeling + +Similarly, you can label operations submitted to a queue: + +[source,cpp] +---- +void submit_work(vk::raii::Queue& queue, vk::raii::CommandBuffer& cmd_buffer) { + // Begin a labeled region for the queue submission + vk::DebugUtilsLabelEXT label_info{}; + label_info.setPLabelName("Frame Rendering"); + label_info.setColor(std::array{0.0f, 0.5f, 1.0f, 1.0f}); // Blue for frame + queue.beginDebugUtilsLabelEXT(label_info); + + // Submit the command buffer + vk::SubmitInfo submit_info{}; + submit_info.setCommandBufferCount(1); + submit_info.setPCommandBuffers(&(*cmd_buffer)); + queue.submit(submit_info, nullptr); + + // End the labeled region + queue.endDebugUtilsLabelEXT(); +} +---- + +=== Using RenderDoc + +RenderDoc is a graphics frame debugger and capture/analysis tool (not a compiler). It allows you to capture frames from your application and analyze them in detail. It's particularly useful for Vulkan applications due to its comprehensive support for the API. + + +==== Integrating RenderDoc with Your Application + +You can integrate RenderDoc directly into your application using its in-application API: + +[source,cpp] +---- +import std; +import vulkan_raii; + +#include + +// Load the RenderDoc API +RENDERDOC_API_1_4_1* renderdoc_api = nullptr; + +bool load_renderdoc_api() { + #if defined(_WIN32) + HMODULE renderdoc_module = LoadLibraryA("renderdoc.dll"); + #else + void* renderdoc_module = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD); + #endif + + if (!renderdoc_module) { + std::cerr << "RenderDoc not loaded in this application" << std::endl; + return false; + } + + #if defined(_WIN32) + pRENDERDOC_GetAPI get_api = (pRENDERDOC_GetAPI)GetProcAddress(renderdoc_module, "RENDERDOC_GetAPI"); + #else + pRENDERDOC_GetAPI get_api = (pRENDERDOC_GetAPI)dlsym(renderdoc_module, "RENDERDOC_GetAPI"); + #endif + + if (!get_api) { + std::cerr << "Failed to get RenderDoc API function" << std::endl; + return false; + } + + int ret = get_api(eRENDERDOC_API_Version_1_4_1, (void**)&renderdoc_api); + if (ret != 1) { + std::cerr << "Failed to initialize RenderDoc API" << std::endl; + return false; + } + + std::cout << "RenderDoc API initialized successfully" << std::endl; + return true; +} + +// Trigger a capture +void capture_frame() { + if (renderdoc_api) { + renderdoc_api->TriggerCapture(); + } +} +---- + +==== Analyzing Captures + +Once you've captured a frame, you can analyze it in the RenderDoc application. Here are some key features to look for: + +1. *Pipeline State*: Examine the full graphics pipeline state for each draw call +2. *Resource Inspection*: View the contents of buffers, textures, and other resources +3. *Shader Debugging*: Step through shader execution for specific pixels +4. *Timing Information*: Analyze performance of different parts of your frame + +==== Best Practices for RenderDoc + +To get the most out of RenderDoc: + +1. *Use Object Names*: As discussed earlier, naming your Vulkan objects makes them much easier to identify in RenderDoc (you'll see them in the Resource Inspector and Pipeline State views). +2. *Use Command Buffer Labels*: These appear in RenderDoc's Event Browser and help you navigate to the relevant draw/dispatch quickly. +3. *Capture the Problem Frame*: Trigger a capture exactly when the issue occurs (via hotkey or the in-application API) to minimize unrelated events and noise. +4. *Minimize to a Repro*: Create a minimal reproducible scene or toggle features off to isolate the problem. If you reduce resolution, make sure it doesn't alter ordering/timing in a way that hides the bug. + +=== Combining VK_KHR_debug_utils and RenderDoc + +The real power comes from combining these approaches: + +1. Use VK_KHR_debug_utils to add rich debugging information to your application +2. Use RenderDoc to capture and analyze frames with this information +3. Use validation layers to catch API usage errors + +Here's an example of setting up a debugging environment that combines these approaches: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class DebugManager { +public: + DebugManager() { + // Try to load RenderDoc API + load_renderdoc_api(); + } + + void setup_instance_debugging(vk::raii::Context& context, vk::InstanceCreateInfo& create_info) { + // Add validation layers + std::vector validation_layers = {"VK_LAYER_KHRONOS_validation"}; + create_info.setPEnabledLayerNames(validation_layers); + + // Add debug utils extension + std::vector extensions = {VK_EXT_DEBUG_UTILS_EXTENSION_NAME}; + // Add any existing extensions + if (create_info.enabledExtensionCount > 0) { + for (uint32_t i = 0; i < create_info.enabledExtensionCount; i++) { + extensions.push_back(create_info.ppEnabledExtensionNames[i]); + } + } + create_info.setPEnabledExtensionNames(extensions); + + // Store debug messenger create info for instance creation + debug_create_info.setMessageSeverity( + vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning | + vk::DebugUtilsMessageSeverityFlagBitsEXT::eError + ); + debug_create_info.setMessageType( + vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral | + vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation | + vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance + ); + debug_create_info.setPfnUserCallback(debug_callback); + + // Add to pNext chain + debug_create_info.pNext = create_info.pNext; + create_info.pNext = &debug_create_info; + } + + void setup_debug_messenger(vk::raii::Instance& instance) { + debug_messenger = vk::raii::DebugUtilsMessengerEXT(instance, debug_create_info); + } + + template + void set_name(vk::raii::Device& device, T handle, const std::string& name) { + try { + vk::DebugUtilsObjectNameInfoEXT name_info{}; + name_info.setObjectType(get_object_type()); + name_info.setObjectHandle(reinterpret_cast(static_cast(handle))); + name_info.setPObjectName(name.c_str()); + + device.setDebugUtilsObjectNameEXT(name_info); + } catch (vk::SystemError& err) { + std::cerr << "Failed to set object name: " << err.what() << std::endl; + } + } + + void capture_next_frame() { + if (renderdoc_api) { + renderdoc_api->TriggerCapture(); + } + } + +private: + vk::DebugUtilsMessengerCreateInfoEXT debug_create_info{}; + vk::raii::DebugUtilsMessengerEXT debug_messenger{nullptr}; + RENDERDOC_API_1_4_1* renderdoc_api = nullptr; +}; +---- + +=== Conclusion + +Effective debugging is essential for developing complex Vulkan applications. By combining the power of VK_KHR_debug_utils for in-application debugging and RenderDoc for frame capture and analysis, you can quickly identify and fix issues in your rendering pipeline. + +In the next section, we'll explore crash handling and minidumps, which are crucial for diagnosing issues that occur in production environments. + +link:02_cicd.adoc[Previous: CI/CD for Vulkan Projects] | link:04_crash_minidump.adoc[Next: Crash Handling and Minidumps] diff --git a/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc b/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc new file mode 100644 index 00000000..84dde25b --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/04_crash_minidump.adoc @@ -0,0 +1,533 @@ +:pp: {plus}{plus} + += Tooling: Crash Handling and GPU Crash Dumps + +== Crash Handling in Vulkan Applications + +Even with thorough testing and debugging, crashes can still occur in production environments. When they do, having robust crash handling mechanisms can help you diagnose and fix issues quickly. This chapter focuses on practical GPU crash diagnostics (e.g., NVIDIA Nsight Aftermath, AMD Radeon GPU Detective) and clarifies the role and limitations of OS process minidumps, which usually lack GPU state and are rarely sufficient to root-cause graphics/device-lost issues on their own. + +=== Understanding Crashes in Vulkan Applications + +Vulkan applications can crash for various reasons: + +1. *API Usage Errors*: Incorrect use of the Vulkan API that validation layers would catch in debug builds +2. *Driver Bugs*: Issues in the GPU driver that may only manifest with specific hardware or workloads +3. *Resource Management Issues*: Memory leaks, double frees, or accessing destroyed resources +4. *Shader Errors*: Runtime errors in shaders that cause the GPU to hang +5. *System-Level Issues*: Out of memory conditions, system instability, etc. + +Let's explore how to handle these crashes and gather diagnostic information. + +=== Implementing Basic Crash Handling + +First, let's implement a basic crash handler that can catch unhandled exceptions and segmentation faults: + +[source,cpp] +---- +import std; +import vulkan_raii; + +// Global state for crash handling +namespace crash_handler { + std::string app_name; + std::string crash_log_path; + bool initialized = false; + + // Log basic system information + void log_system_info(std::ofstream& log) { + log << "Application: " << app_name << std::endl; + log << "Timestamp: " << std::chrono::system_clock::now() << std::endl; + + // Log OS information + #if defined(_WIN32) + log << "OS: Windows" << std::endl; + #elif defined(__linux__) + log << "OS: Linux" << std::endl; + #elif defined(__APPLE__) + log << "OS: macOS" << std::endl; + #else + log << "OS: Unknown" << std::endl; + #endif + + // Log CPU information + log << "CPU Cores: " << std::thread::hardware_concurrency() << std::endl; + + // Log memory information + #if defined(_WIN32) + MEMORYSTATUSEX mem_info; + mem_info.dwLength = sizeof(MEMORYSTATUSEX); + GlobalMemoryStatusEx(&mem_info); + log << "Total Physical Memory: " << mem_info.ullTotalPhys / (1024 * 1024) << " MB" << std::endl; + log << "Available Memory: " << mem_info.ullAvailPhys / (1024 * 1024) << " MB" << std::endl; + #elif defined(__linux__) + // Linux-specific memory info code + #elif defined(__APPLE__) + // macOS-specific memory info code + #endif + } + + // Log Vulkan-specific information + void log_vulkan_info(std::ofstream& log, vk::raii::PhysicalDevice* physical_device = nullptr) { + if (physical_device) { + auto properties = physical_device->getProperties(); + log << "GPU: " << properties.deviceName << std::endl; + log << "Driver Version: " << properties.driverVersion << std::endl; + log << "Vulkan API Version: " + << VK_VERSION_MAJOR(properties.apiVersion) << "." + << VK_VERSION_MINOR(properties.apiVersion) << "." + << VK_VERSION_PATCH(properties.apiVersion) << std::endl; + } else { + log << "No Vulkan physical device information available" << std::endl; + } + } + + // Handler for unhandled exceptions + void handle_exception(const std::exception& e, vk::raii::PhysicalDevice* physical_device = nullptr) { + try { + std::ofstream log(crash_log_path, std::ios::app); + log << "==== Crash Report ====" << std::endl; + log_system_info(log); + log_vulkan_info(log, physical_device); + + log << "Exception: " << e.what() << std::endl; + log << "==== End of Crash Report ====" << std::endl << std::endl; + + log.close(); + } catch (...) { + // Last resort if we can't even write to the log + std::cerr << "Failed to write crash log" << std::endl; + } + } + + // Signal handler for segfaults, etc. + void signal_handler(int signal) { + try { + std::ofstream log(crash_log_path, std::ios::app); + log << "==== Crash Report ====" << std::endl; + log_system_info(log); + + log << "Signal: " << signal << " ("; + switch (signal) { + case SIGSEGV: log << "SIGSEGV - Segmentation fault"; break; + case SIGILL: log << "SIGILL - Illegal instruction"; break; + case SIGFPE: log << "SIGFPE - Floating point exception"; break; + case SIGABRT: log << "SIGABRT - Abort"; break; + default: log << "Unknown signal"; break; + } + log << ")" << std::endl; + + log << "==== End of Crash Report ====" << std::endl << std::endl; + + log.close(); + } catch (...) { + // Last resort if we can't even write to the log + std::cerr << "Failed to write crash log" << std::endl; + } + + // Re-raise the signal for the default handler + signal(signal, SIG_DFL); + raise(signal); + } + + // Initialize the crash handler + void initialize(const std::string& application_name, const std::string& log_path) { + if (initialized) return; + + app_name = application_name; + crash_log_path = log_path; + + // Set up signal handlers + signal(SIGSEGV, signal_handler); + signal(SIGILL, signal_handler); + signal(SIGFPE, signal_handler); + signal(SIGABRT, signal_handler); + + initialized = true; + } +} + +// Example usage in main application +int main() { + try { + // Initialize crash handler + crash_handler::initialize("MyVulkanApp", "crash_log.txt"); + + // Initialize Vulkan + vk::raii::Context context; + auto instance = create_instance(context); + auto physical_device = select_physical_device(instance); + auto device = create_device(physical_device); + + // Main application loop + while (true) { + try { + // Render frame + render_frame(device); + } catch (const vk::SystemError& e) { + // Handle Vulkan errors that we can recover from + std::cerr << "Vulkan error: " << e.what() << std::endl; + } + } + } catch (const std::exception& e) { + // Handle unrecoverable exceptions + crash_handler::handle_exception(e); + return 1; + } + + return 0; +} +---- + +=== GPU Crash Diagnostics (Vulkan) + +While OS process minidumps capture CPU-side state, GPU crashes (device lost, TDRs, hangs) require GPU-specific crash dumps to be actionable. In practice, you’ll want to integrate vendor tooling that can record GPU execution state around the fault. + +==== NVIDIA: Nsight Aftermath (Vulkan) + +Overview: + +- Collects GPU crash dumps with information about the last executed draw/dispatch, bound pipeline/shaders, markers, and resource identifiers. +- Works alongside your Vulkan app; you analyze dumps with NVIDIA tools to pinpoint the failing work and shader. + +Practical steps: + +1. Enable object names and labels + - Use VK_EXT_debug_utils to name pipelines, shaders, images, buffers, and to insert command buffer labels for major passes and draw/dispatch groups. These names surface in crash reports and greatly aid triage. +2. Add frame/work markers + - Insert meaningful labels before/after critical rendering phases. If available on your target, also use vendor checkpoint/marker extensions (e.g., VK_NV_device_diagnostic_checkpoints) to provide fine-grained breadcrumbs. +3. Build shaders with unique IDs and optional debug info + - Ensure each pipeline/shader can be correlated (e.g., include a stable hash/UUID in your pipeline cache and application logs). Keep the mapping from IDs to source for analysis. +4. Initialize and enable GPU crash dumps + - Integrate the Nsight Aftermath Vulkan SDK per NVIDIA’s documentation. Register a callback to receive crash dump data, write it to disk, and include your marker string table for symbolication. +5. Handle device loss + - On VK_ERROR_DEVICE_LOST (or Windows TDR), flush any in-memory marker logs, persist the crash dump, and then terminate cleanly. Attempting to continue rendering is undefined. + +References: NVIDIA Nsight Aftermath SDK and documentation. + +==== AMD: Radeon GPU Detective (RGD) + +- AMD provides tools to capture and analyze GPU crash information on RDNA hardware. Similar principles apply: enable object names, label command buffers, and preserve pipeline/shader identifiers so RGD can point back to your content. +- See AMD Radeon GPU Detective and related documentation for Vulkan integration and analysis workflows. + +==== Vendor-agnostic groundwork that helps all tools + +- Name everything via VK_EXT_debug_utils. +- Insert command buffer labels at meaningful boundaries (frame, pass, material batch, etc.). +- Persist build/version, driver, Vulkan API/UUID, and pipeline cache UUID in your logs and crash artifacts. +- Implement robust device lost handling: stop submitting, free/teardown safely, write artifacts, exit. + +=== Generating Minidumps + +Use OS process minidumps to capture CPU-side call stacks, threads, and memory snapshots at the time of a crash. For graphics issues and device loss, they rarely contain the GPU execution state you need—treat minidumps as a complement to GPU crash dumps, not a replacement. + +Below is a brief outline for generating minidumps with platform APIs (useful for correlating CPU context with a GPU crash): + +[source,cpp] +---- +import std; +import vulkan_raii; + +namespace crash_handler { + std::string app_name; + std::string dump_path; + bool initialized = false; + + #if defined(_WIN32) + // Windows implementation using Windows Error Reporting (WER) + LONG WINAPI windows_exception_handler(EXCEPTION_POINTERS* exception_pointers) { + // Create a unique filename for the minidump + std::string filename = dump_path + "\\" + app_name + "_" + + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()) + ".dmp"; + + // Create the minidump file + HANDLE file = CreateFileA( + filename.c_str(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr + ); + + if (file != INVALID_HANDLE_VALUE) { + // Initialize minidump info + MINIDUMP_EXCEPTION_INFORMATION exception_info; + exception_info.ThreadId = GetCurrentThreadId(); + exception_info.ExceptionPointers = exception_pointers; + exception_info.ClientPointers = FALSE; + + // Write the minidump + MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + file, + MiniDumpWithFullMemory, // Dump type + &exception_info, + nullptr, + nullptr + ); + + CloseHandle(file); + + std::cerr << "Minidump written to: " << filename << std::endl; + } else { + std::cerr << "Failed to create minidump file" << std::endl; + } + + // Continue with normal exception handling + return EXCEPTION_CONTINUE_SEARCH; + } + + void initialize(const std::string& application_name, const std::string& minidump_path) { + if (initialized) return; + + app_name = application_name; + dump_path = minidump_path; + + // Create the dump directory if it doesn't exist + CreateDirectoryA(dump_path.c_str(), nullptr); + + // Set up the exception handler + SetUnhandledExceptionFilter(windows_exception_handler); + + initialized = true; + } + + #elif defined(__linux__) + // Linux implementation using Google Breakpad + // Note: This requires linking against the Google Breakpad library + + #include "client/linux/handler/exception_handler.h" + + // Callback for when a minidump is generated + static bool minidump_callback(const google_breakpad::MinidumpDescriptor& descriptor, + void* context, bool succeeded) { + std::cerr << "Minidump generated: " << descriptor.path() << std::endl; + return succeeded; + } + + google_breakpad::ExceptionHandler* exception_handler = nullptr; + + void initialize(const std::string& application_name, const std::string& minidump_path) { + if (initialized) return; + + app_name = application_name; + dump_path = minidump_path; + + // Create the dump directory if it doesn't exist + std::filesystem::create_directories(dump_path); + + // Set up the exception handler + google_breakpad::MinidumpDescriptor descriptor(dump_path); + exception_handler = new google_breakpad::ExceptionHandler( + descriptor, + nullptr, + minidump_callback, + nullptr, + true, + -1 + ); + + initialized = true; + } + + #elif defined(__APPLE__) + // macOS implementation using Google Breakpad + // Similar to Linux implementation + #endif +} +---- + +=== Analyzing Minidumps + +Minidumps are best used to understand CPU-side state around a crash (e.g., which thread faulted, call stacks leading to vkQueueSubmit/vkQueuePresent, allocator misuse) and to correlate with a GPU crash dump from vendor tools. Here’s a brief workflow on different platforms: + +==== Windows + +On Windows, you can use Visual Studio or WinDbg to analyze minidumps: + +1. *Visual Studio*: + - Open Visual Studio + - Go to File > Open > File and select the .dmp file + - Visual Studio will load the minidump and show the call stack at the time of the crash + +2. *WinDbg*: + - Open WinDbg + - Open the minidump file + - Use commands like `.ecxr` to examine the exception context record + - Use `k` to view the call stack + +==== Linux and macOS + +On Linux and macOS, you can use tools like GDB or LLDB to analyze minidumps generated by Google Breakpad: + +1. *Using minidump_stackwalk* (part of Google Breakpad): + ``` + minidump_stackwalk minidump_file.dmp /path/to/symbols > stacktrace.txt + ``` + +2. *Using GDB*: + ``` + gdb /path/to/executable + (gdb) core-file /path/to/minidump + (gdb) bt + ``` + +=== Vulkan-Specific Crash Information + +For Vulkan applications, it's helpful to include additional information in your crash reports: + +[source,cpp] +---- +void log_vulkan_detailed_info(std::ofstream& log, vk::raii::PhysicalDevice& physical_device, + vk::raii::Device& device) { + // Log physical device properties + auto properties = physical_device.getProperties(); + log << "GPU: " << properties.deviceName << std::endl; + log << "Driver Version: " << properties.driverVersion << std::endl; + log << "Vulkan API Version: " + << VK_VERSION_MAJOR(properties.apiVersion) << "." + << VK_VERSION_MINOR(properties.apiVersion) << "." + << VK_VERSION_PATCH(properties.apiVersion) << std::endl; + + // Log memory usage + auto memory_properties = physical_device.getMemoryProperties(); + log << "Memory Heaps:" << std::endl; + for (uint32_t i = 0; i < memory_properties.memoryHeapCount; i++) { + log << " Heap " << i << ": " + << (memory_properties.memoryHeaps[i].size / (1024 * 1024)) << " MB"; + if (memory_properties.memoryHeaps[i].flags & vk::MemoryHeapFlagBits::eDeviceLocal) { + log << " (Device Local)"; + } + log << std::endl; + } + + // Log enabled extensions + auto extensions = device.enumerateDeviceExtensionProperties(); + log << "Enabled Extensions:" << std::endl; + for (const auto& ext : extensions) { + log << " " << ext.extensionName << " (version " << ext.specVersion << ")" << std::endl; + } + + // Log current pipeline cache state + // This can be useful for diagnosing shader-related crashes + try { + auto pipeline_cache_data = device.getPipelineCacheData(); + log << "Pipeline Cache Size: " << pipeline_cache_data.size() << " bytes" << std::endl; + } catch (const vk::SystemError& e) { + log << "Failed to get pipeline cache data: " << e.what() << std::endl; + } +} +---- + +=== Integrating with Telemetry Systems + +For production applications, you might want to automatically upload crash reports to a telemetry system for analysis: + +[source,cpp] +---- +import std; +import vulkan_raii; +#include + +namespace crash_handler { + // ... existing code ... + + std::string telemetry_url; + bool telemetry_enabled = false; + + // Upload a minidump to the telemetry server + bool upload_minidump(const std::string& minidump_path) { + if (!telemetry_enabled || telemetry_url.empty()) { + return false; + } + + CURL* curl = curl_easy_init(); + if (!curl) { + std::cerr << "Failed to initialize curl" << std::endl; + return false; + } + + // Set up the form data + curl_mime* form = curl_mime_init(curl); + + // Add the minidump file + curl_mimepart* field = curl_mime_addpart(form); + curl_mime_name(field, "minidump"); + curl_mime_filedata(field, minidump_path.c_str()); + + // Add application information + field = curl_mime_addpart(form); + curl_mime_name(field, "product"); + curl_mime_data(field, app_name.c_str(), CURL_ZERO_TERMINATED); + + // Add version information + field = curl_mime_addpart(form); + curl_mime_name(field, "version"); + curl_mime_data(field, "1.0.0", CURL_ZERO_TERMINATED); // Replace with your version + + // Set up the request + curl_easy_setopt(curl, CURLOPT_URL, telemetry_url.c_str()); + curl_easy_setopt(curl, CURLOPT_MIMEPOST, form); + + // Perform the request + CURLcode res = curl_easy_perform(curl); + + // Clean up + curl_mime_free(form); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + std::cerr << "Failed to upload minidump: " << curl_easy_strerror(res) << std::endl; + return false; + } + + return true; + } + + // Enable telemetry + void enable_telemetry(const std::string& url) { + telemetry_url = url; + telemetry_enabled = true; + + // Initialize curl + curl_global_init(CURL_GLOBAL_ALL); + } + + // Disable telemetry + void disable_telemetry() { + telemetry_enabled = false; + + // Clean up curl + curl_global_cleanup(); + } +} +---- + +=== Best Practices for Crash Handling (Vulkan/GPU-focused) + +To make crash data actionable for graphics issues, prefer these concrete steps: + +1. Name and label aggressively + - Use VK_EXT_debug_utils to name all objects and insert command buffer labels at pass/material boundaries and before large draw/dispatch batches. Persist a small in-memory ring buffer of recent labels for inclusion in crash artifacts. +2. Prepare for device loss + - Implement a central handler for VK_ERROR_DEVICE_LOST. Stop submitting work, flush logs/markers, request vendor GPU crash dump data, and exit. Avoid attempting recovery in the same process unless you have a robust reinitialization path. +3. Capture GPU crash dumps on supported hardware + - Integrate NVIDIA Nsight Aftermath and/or AMD RGD depending on your target audience. Ship with crash dumps enabled in development/beta builds; provide a toggle for users. +4. Make builds symbol-friendly + - Keep a mapping from pipeline/shader hashes to source/IR/SPIR-V and build IDs. Enable shader debug info where feasible for diagnosis builds. +5. Record environment info + - Log driver version, Vulkan version, GPU name/PCI ID, pipeline cache UUID, app build/version, and relevant feature toggles. Include this alongside minidumps and GPU crash dumps. +6. Reproduce deterministically + - Provide a way to disable background variability (e.g., async streaming) and to replay a captured sequence of commands/scenes to reproduce the crash locally. +7. Respect privacy and distribution concerns + - Clearly document what crash data is collected (minidumps, GPU crash dumps, logs) and require opt‑in for uploads. Strip user-identifiable data. + +=== Conclusion + +Robust crash handling is essential for maintaining a high-quality Vulkan application. Combine vendor GPU crash dumps (Aftermath, RGD, etc.) with CPU-side minidumps and thorough logging to quickly diagnose and fix issues in production. Treat minidumps as complementary context; the actionable details for graphics faults typically come from GPU crash dump tooling. + +In the next section, we'll explore Vulkan extensions for robustness, which can reduce undefined behavior and help prevent crashes in the first place. + +link:03_debugging_and_renderdoc.adoc[Previous: Debugging with VK_KHR_debug_utils and RenderDoc] | link:05_extensions.adoc[Next: Vulkan Extensions for Robustness] diff --git a/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc b/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc new file mode 100644 index 00000000..47db7e90 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/05_extensions.adoc @@ -0,0 +1,377 @@ +:pp: {plus}{plus} + += Tooling: Vulkan Extensions for Robustness + +== Vulkan Extensions for Robustness + +Vulkan's explicit design gives developers fine-grained control over the graphics pipeline, but this control comes with responsibility. Undefined behavior can occur when applications make mistakes like accessing out-of-bounds memory or using uninitialized resources. In this section, we'll explore Vulkan extensions that can help make your application more robust against such issues, with a particular focus on VK_EXT_robustness2. + +=== Understanding Undefined Behavior in Vulkan + +Before diving into robustness extensions, let's understand what kinds of undefined behavior can occur in Vulkan applications: + +1. *Out-of-bounds Access*: Accessing memory outside the bounds of a buffer or image +2. *Use-after-free*: Using a resource after it has been destroyed +3. *Uninitialized Memory*: Reading from memory that hasn't been initialized +4. *Invalid Descriptors*: Using descriptors that point to invalid or incompatible resources +5. *Shader Execution Errors*: Division by zero, infinite loops, etc. + +In standard Vulkan, these errors can lead to unpredictable behavior, including: + +* Application crashes +* GPU hangs requiring a system restart +* Corrupted rendering +* Security vulnerabilities +* Inconsistent behavior across different hardware + +Robustness extensions aim to provide more predictable behavior in these scenarios, often at a small performance cost. + +=== VK_EXT_robustness2 Extension + +The VK_EXT_robustness2 extension is an improved version of the original VK_EXT_robustness extension. It provides more comprehensive protection against undefined behavior, particularly for out-of-bounds accesses. + +==== Key Features + +VK_EXT_robustness2 offers several important features: + +1. *Robust Buffer Access*: Out-of-bounds reads from buffers return zero values instead of causing undefined behavior +2. *Robust Image Access*: Out-of-bounds reads from images return zero or transparent black +3. *Null Descriptor Handling*: Reads from null descriptors return zero values +4. *Robust Buffer Access 2*: An improved version that also handles out-of-bounds writes by discarding them + +==== Enabling VK_EXT_robustness2 + +Let's see how to enable and use this extension. + +[source,cpp] +---- +bool check_robustness2_support(vk::raii::PhysicalDevice& physical_device) { + // Check if the extension is supported + auto available_extensions = physical_device.enumerateDeviceExtensionProperties(); + + for (const auto& extension : available_extensions) { + if (strcmp(extension.extensionName, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) == 0) { + return true; + } + } + + return false; +} + +void enable_robustness2(vk::DeviceCreateInfo& device_create_info, + std::vector& enabled_extensions) { + // Add the extension to the list of enabled extensions + enabled_extensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); + device_create_info.setPEnabledExtensionNames(enabled_extensions); + + // Set up the robustness2 features + vk::PhysicalDeviceRobustness2FeaturesEXT robustness2_features{}; + robustness2_features.setRobustBufferAccess2(VK_TRUE); + robustness2_features.setRobustImageAccess2(VK_TRUE); + robustness2_features.setNullDescriptor(VK_TRUE); + + // Add to the pNext chain + robustness2_features.pNext = device_create_info.pNext; + device_create_info.pNext = &robustness2_features; +} + +vk::raii::Device create_robust_device(vk::raii::PhysicalDevice& physical_device, + vk::raii::Instance& instance) { + // Check for support + if (!check_robustness2_support(physical_device)) { + std::cerr << "VK_EXT_robustness2 is not supported on this device" << std::endl; + // Fall back to less robust behavior or abort + } + + // Set up device creation + std::vector enabled_extensions; + // Add your other required extensions here + + vk::DeviceCreateInfo create_info{}; + // Set up your queues, features, etc. + + // Enable robustness2 + enable_robustness2(create_info, enabled_extensions); + + // Create the device + return vk::raii::Device(physical_device, create_info); +} +---- + +==== Using Robust Access in Practice + +Once you've enabled the extension, robust buffer and image access will be applied automatically. However, you should be aware of some considerations: + +1. *Performance Impact*: Robust access can have a performance cost, as the GPU needs to perform bounds checking +2. *Not a Substitute for Correctness*: While robustness extensions make your application more resilient, they don't fix the underlying bugs +3. *Debug vs. Release*: Consider enabling robustness in debug builds for development and testing, but evaluate the performance impact for release builds + +Here's an example of how robust buffer access can prevent crashes: + +[source,cpp] +---- +// Without robust buffer access, this could crash or produce undefined results +void potentially_dangerous_operation(vk::raii::CommandBuffer& cmd_buffer, + vk::raii::Buffer& buffer, + vk::raii::DescriptorSet& descriptor_set, + uint32_t dynamic_offset, + uint32_t buffer_size) { + // If dynamic_offset is too large, this would normally cause undefined behavior + // With robust buffer access, out-of-bounds reads will return zero + cmd_buffer.bindDescriptorSets( + vk::PipelineBindPoint::eCompute, + pipeline_layout, + 0, + 1, + &(*descriptor_set), + 1, + &dynamic_offset + ); + + // Dispatch compute work that might read out of bounds + cmd_buffer.dispatch(buffer_size / 64 + 1, 1, 1); // Potentially too many workgroups +} +---- + +=== Other Robustness Extensions + +While VK_EXT_robustness2 is the focus of this section, there are other extensions that can help improve application robustness: + +==== VK_KHR_buffer_device_address + +This extension allows you to use physical device addresses for buffers, which can be useful for advanced techniques. It includes robustness features for handling invalid addresses (when combined with robust access features like VK_EXT_robustness2 or core robustBufferAccess): + +[source,cpp] +---- +void enable_buffer_device_address(vk::DeviceCreateInfo& device_create_info, + std::vector& enabled_extensions) { + enabled_extensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + device_create_info.setPEnabledExtensionNames(enabled_extensions); + + // Enable Buffer Device Address features + vk::PhysicalDeviceBufferDeviceAddressFeatures buffer_device_address_features{}; + buffer_device_address_features.setBufferDeviceAddress(VK_TRUE); + buffer_device_address_features.setBufferDeviceAddressCaptureReplay(VK_TRUE); + + // Optionally chain robustness features to ensure invalid addresses read as zero and writes are discarded + // (If you've already enabled VK_EXT_robustness2 elsewhere, this is not required here.) + vk::PhysicalDeviceRobustness2FeaturesEXT robustness2_features{}; + robustness2_features.setRobustBufferAccess2(VK_TRUE); + robustness2_features.setRobustImageAccess2(VK_TRUE); + robustness2_features.setNullDescriptor(VK_TRUE); + + // Chain features: robustness2 -> BDA -> existing pNext + robustness2_features.pNext = &buffer_device_address_features; + buffer_device_address_features.pNext = device_create_info.pNext; + device_create_info.pNext = &robustness2_features; +} +---- + +==== VK_EXT_descriptor_indexing + +This extension allows for more flexible descriptor indexing, including robustness-related capabilities such as tolerating out-of-bounds indices (reads become zero when robust access is enabled), partially bound descriptor sets, and update-after-bind. To actually make use of these behaviors you need to enable both device features and descriptor set layout binding flags: + +[source,cpp] +---- +void enable_descriptor_indexing(vk::DeviceCreateInfo& device_create_info, + std::vector& enabled_extensions) { + enabled_extensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + device_create_info.setPEnabledExtensionNames(enabled_extensions); + + vk::PhysicalDeviceDescriptorIndexingFeatures indexing_features{}; + // Shader indexing capabilities (commonly needed alongside robustness) + indexing_features.setShaderSampledImageArrayNonUniformIndexing(VK_TRUE); + indexing_features.setShaderStorageBufferArrayNonUniformIndexing(VK_TRUE); + + // Robustness-enabling behaviors + indexing_features.setRuntimeDescriptorArray(VK_TRUE); + indexing_features.setDescriptorBindingPartiallyBound(VK_TRUE); + indexing_features.setDescriptorBindingSampledImageUpdateAfterBind(VK_TRUE); + indexing_features.setDescriptorBindingStorageBufferUpdateAfterBind(VK_TRUE); + indexing_features.setDescriptorBindingUpdateUnusedWhilePending(VK_TRUE); + + // Add to the pNext chain (can be chained together with VK_EXT_robustness2) + indexing_features.pNext = device_create_info.pNext; + device_create_info.pNext = &indexing_features; +} +---- + +For descriptor arrays, you must also specify binding flags at layout creation time: + +[source,cpp] +---- +// Example: descriptor set layout with a runtime-sized array that can be partially bound +vk::DescriptorSetLayoutBinding binding{}; +binding.binding = 0; +binding.descriptorType = vk::DescriptorType::eCombinedImageSampler; +binding.descriptorCount = 128; // example array size; for true runtime arrays also enable variable descriptor counts +binding.stageFlags = vk::ShaderStageFlagBits::eFragment; + +vk::DescriptorBindingFlags binding_flags = + vk::DescriptorBindingFlagBits::ePartiallyBound | + vk::DescriptorBindingFlagBits::eUpdateAfterBind; + +vk::DescriptorSetLayoutBindingFlagsCreateInfo flags_ci{}; +flags_ci.setBindingCount(1); +flags_ci.setPBindingFlags(&binding_flags); + +vk::DescriptorSetLayoutCreateInfo dsl_ci{}; +dsl_ci.setPBindings(&binding); +dsl_ci.setBindingCount(1); +// Required when using update-after-bind flags +// (some descriptor types require pool and layout flags to match update-after-bind usage) +dsl_ci.flags |= vk::DescriptorSetLayoutCreateFlagBits::eUpdateAfterBindPool; + +dsl_ci.pNext = &flags_ci; + +vk::raii::DescriptorSetLayout set_layout{device, dsl_ci}; +---- + +If you need truly variable-length descriptor arrays at runtime, also enable variable descriptor counts and use the corresponding allocate info: + +[source,cpp] +---- +// Enable the device feature +// indexing_features.setDescriptorBindingVariableDescriptorCount(VK_TRUE); // do this where features are enabled + +uint32_t max_descriptors_for_set0 = 1024; // requested at allocation time + +vk::DescriptorSetVariableDescriptorCountAllocateInfo variable_counts_info{}; +variable_counts_info.setDescriptorSetCount(1); +variable_counts_info.setPDescriptorCounts(&max_descriptors_for_set0); + +vk::DescriptorSetAllocateInfo alloc_info{}; +alloc_info.setDescriptorPool(descriptor_pool); +alloc_info.setDescriptorSetCount(1); +alloc_info.setPSetLayouts(&*set_layout); +alloc_info.pNext = &variable_counts_info; + +auto descriptor_sets = vk::raii::DescriptorSets{device, alloc_info}; +---- + +Note: With VK_EXT_robustness2's nullDescriptor = VK_TRUE and descriptor indexing's partially-bound behavior, unbound array elements will read as zero rather than invoking undefined behavior. + +=== Combining Robustness Extensions with Debugging Tools + +For maximum effectiveness, combine robustness extensions with the debugging tools we discussed in previous sections: + +[source,cpp] +---- +class RobustVulkanApplication { +public: + RobustVulkanApplication() { + initialize_vulkan(); + } + + void run() { + // Main application loop + while (!should_close()) { + try { + update(); + render(); + } catch (const vk::SystemError& e) { + // Handle recoverable Vulkan errors + std::cerr << "Vulkan error: " << e.what() << std::endl; + // Attempt recovery + if (!recover_from_error()) { + break; + } + } + } + + cleanup(); + } + +private: + void initialize_vulkan() { + // Create instance with validation layers in debug builds + #ifdef _DEBUG + enable_validation_layers = true; + #else + enable_validation_layers = false; + #endif + + instance = create_instance(); + + // Set up debug messenger if validation is enabled + if (enable_validation_layers) { + debug_messenger = create_debug_messenger(instance); + } + + // Select physical device + physical_device = select_physical_device(instance); + + // Check for robustness support + has_robustness2 = check_robustness2_support(physical_device); + + // Create logical device with robustness if available + device = create_device(physical_device); + + // Initialize other Vulkan resources + // ... + } + + vk::raii::Device create_device(vk::raii::PhysicalDevice& physical_device) { + std::vector extensions; + // Add required extensions + + vk::DeviceCreateInfo create_info{}; + // Set up queues, etc. + + // Enable robustness if available + if (has_robustness2) { + enable_robustness2(create_info, extensions); + } + + // Enable other robustness-related extensions + enable_buffer_device_address(create_info, extensions); + enable_descriptor_indexing(create_info, extensions); + + return vk::raii::Device(physical_device, create_info); + } + + bool recover_from_error() { + // Attempt to recover from errors + // This might involve recreating swapchain, command buffers, etc. + try { + // Reset command buffers + // Recreate swapchain if needed + // ... + return true; + } catch (const std::exception& e) { + std::cerr << "Failed to recover: " << e.what() << std::endl; + return false; + } + } + + // Vulkan objects + vk::raii::Context context; + vk::raii::Instance instance{nullptr}; + vk::raii::DebugUtilsMessengerEXT debug_messenger{nullptr}; + vk::raii::PhysicalDevice physical_device{nullptr}; + vk::raii::Device device{nullptr}; + + // Flags + bool enable_validation_layers = false; + bool has_robustness2 = false; +}; +---- + +=== Best Practices for Using Robustness Extensions + +To make the most of robustness extensions: + +1. *Check for Support*: Always check if the extension is supported before trying to use it +2. *Fallback Behavior*: Implement fallback behavior for devices that don't support the extensions +3. *Performance Testing*: Measure the performance impact of enabling robustness features +4. *Combine with Validation*: Use validation layers during development to catch issues early +5. *Don't Rely on Robustness*: Fix the underlying issues rather than relying on robustness extensions to mask them +6. *Document Usage*: Clearly document which extensions your application requires and why + +=== Conclusion + +Vulkan robustness extensions, particularly VK_EXT_robustness2, provide valuable tools for making your application more resilient to undefined behavior. By combining these extensions with proper error handling, validation layers, and debugging tools, you can create a more stable and reliable Vulkan application. + +In the next and final section, we'll summarize what we've learned about tooling for Vulkan applications and discuss how to apply these techniques in your own projects. + +link:04_crash_minidump.adoc[Previous: Crash Handling and Minidumps] | link:06_packaging_and_distribution.adoc[Next: Packaging and Distribution] diff --git a/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc b/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc new file mode 100644 index 00000000..dbc8dff3 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/06_packaging_and_distribution.adoc @@ -0,0 +1,541 @@ +:pp: {plus}{plus} + += Tooling: Packaging and Distribution + +== Packaging and Distributing Vulkan Applications + +After developing and testing your Vulkan application, the final step is to package and distribute it to users. This process involves preparing your application for different platforms, handling dependencies, and creating installers or packages that provide a smooth installation experience. In this section, we'll explore the key considerations and techniques for packaging and distributing Vulkan applications. + +=== Platform-Specific Packaging Considerations + +Each platform has its own packaging formats and distribution mechanisms. Let's explore the considerations for the major platforms: + +==== Windows Packaging + +On Windows, common packaging formats include: + +* *Executable Installers*: Created with tools like NSIS (Nullsoft Scriptable Install System), Inno Setup, or WiX Toolset +* *MSIX Packages*: Modern Windows app packages that support clean installation and uninstallation +* *Portable Applications*: Self-contained applications that don't require installation + +Here's an example of creating a basic NSIS installer script for a Vulkan application: + +[source,nsis] +---- +; Basic NSIS installer script for a Vulkan application + +!include "MUI2.nsh" + +Name "My Vulkan Application" +OutFile "MyVulkanApp_Installer.exe" +InstallDir "$PROGRAMFILES\MyVulkanApp" + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +Section "Install" + SetOutPath "$INSTDIR" + + ; Application files + File "MyVulkanApp.exe" + File "*.dll" + File /r "shaders" + File /r "assets" + + ; Vulkan Runtime + File "vulkan-1.dll" + + ; Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + + ; Create shortcuts + CreateDirectory "$SMPROGRAMS\MyVulkanApp" + CreateShortcut "$SMPROGRAMS\MyVulkanApp\MyVulkanApp.lnk" "$INSTDIR\MyVulkanApp.exe" + CreateShortcut "$SMPROGRAMS\MyVulkanApp\Uninstall.lnk" "$INSTDIR\Uninstall.exe" +SectionEnd + +Section "Uninstall" + ; Remove application files + Delete "$INSTDIR\MyVulkanApp.exe" + Delete "$INSTDIR\*.dll" + RMDir /r "$INSTDIR\shaders" + RMDir /r "$INSTDIR\assets" + + ; Remove uninstaller + Delete "$INSTDIR\Uninstall.exe" + + ; Remove shortcuts + Delete "$SMPROGRAMS\MyVulkanApp\MyVulkanApp.lnk" + Delete "$SMPROGRAMS\MyVulkanApp\Uninstall.lnk" + RMDir "$SMPROGRAMS\MyVulkanApp" + + ; Remove install directory + RMDir "$INSTDIR" +SectionEnd +---- + +==== Linux Packaging + +On Linux, common packaging formats include: + +* *DEB Packages*: For Debian-based distributions (Ubuntu, Debian, etc.) +* *RPM Packages*: For Red Hat-based distributions (Fedora, CentOS, etc.) +* *AppImage*: Self-contained applications that run on most Linux distributions +* *Flatpak*: Sandboxed applications with controlled access to system resources +* *Snap*: Universal Linux packages maintained by Canonical + +Here's an example of creating a basic AppImage for a Vulkan application: + +[source,bash] +---- +#!/bin/bash +# Script to create an AppImage for a Vulkan application + +# Create AppDir structure +mkdir -p AppDir/usr/bin +mkdir -p AppDir/usr/lib +mkdir -p AppDir/usr/share/applications +mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps +mkdir -p AppDir/usr/share/metainfo + +# Copy application binary +cp build/MyVulkanApp AppDir/usr/bin/ + +# Copy dependencies (excluding system libraries) +ldd build/MyVulkanApp | grep "=> /" | awk '{print $3}' | xargs -I{} cp -v {} AppDir/usr/lib/ + +# Copy Vulkan loader +cp /usr/lib/libvulkan.so.1 AppDir/usr/lib/ + +# Copy application data +cp -r assets AppDir/usr/share/MyVulkanApp/assets +cp -r shaders AppDir/usr/share/MyVulkanApp/shaders + +# Create desktop file +cat > AppDir/usr/share/applications/MyVulkanApp.desktop << EOF +[Desktop Entry] +Name=My Vulkan Application +Exec=MyVulkanApp +Icon=MyVulkanApp +Type=Application +Categories=Graphics; +EOF + +# Copy icon +cp icon.png AppDir/usr/share/icons/hicolor/256x256/apps/MyVulkanApp.png + +# Create AppStream metadata +cat > AppDir/usr/share/metainfo/MyVulkanApp.appdata.xml << EOF + + + com.example.MyVulkanApp + My Vulkan Application + A Vulkan-powered application + +

+ My Vulkan Application is a high-performance graphics application + built with the Vulkan API. +

+
+ https://example.com/MyVulkanApp + + + +
+EOF + +# Create AppRun script +cat > AppDir/AppRun << EOF +#!/bin/bash +SELF=\$(readlink -f "\$0") +HERE=\$(dirname "\$SELF") +export PATH="\${HERE}/usr/bin:\${PATH}" +export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}" +export VK_LAYER_PATH="\${HERE}/usr/share/vulkan/explicit_layer.d" +export VK_ICD_FILENAMES="\${HERE}/usr/share/vulkan/icd.d/vulkan_icd.json" +"\${HERE}/usr/bin/MyVulkanApp" "$@" +EOF + +chmod +x AppDir/AppRun + +# Download appimagetool +wget -c "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" +chmod +x appimagetool-x86_64.AppImage + +# Create the AppImage +./appimagetool-x86_64.AppImage AppDir MyVulkanApp-x86_64.AppImage +---- + +==== macOS Packaging + +On macOS, common packaging formats include: + +* *Application Bundles (.app)*: The standard format for macOS applications +* *Disk Images (.dmg)*: Mountable disk images containing the application +* *Packages (.pkg)*: Installer packages for more complex installations + +Here's an example of creating a basic macOS application bundle structure for a Vulkan application using MoltenVK: + +[source,bash] +---- +#!/bin/bash +# Script to create a macOS application bundle for a Vulkan application + +# Create bundle structure +mkdir -p MyVulkanApp.app/Contents/MacOS +mkdir -p MyVulkanApp.app/Contents/Resources +mkdir -p MyVulkanApp.app/Contents/Frameworks + +# Copy application binary +cp build/MyVulkanApp MyVulkanApp.app/Contents/MacOS/ + +# Copy MoltenVK framework +cp -R $VULKAN_SDK/macOS/Frameworks/MoltenVK.framework MyVulkanApp.app/Contents/Frameworks/ + +# Copy application resources +cp -r assets MyVulkanApp.app/Contents/Resources/assets +cp -r shaders MyVulkanApp.app/Contents/Resources/shaders +cp icon.icns MyVulkanApp.app/Contents/Resources/ + +# Create Info.plist +cat > MyVulkanApp.app/Contents/Info.plist << EOF + + + + + CFBundleExecutable + MyVulkanApp + CFBundleIconFile + icon.icns + CFBundleIdentifier + com.example.MyVulkanApp + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + My Vulkan Application + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + 1 + NSHighResolutionCapable + + + +EOF + +# Create DMG (optional) +hdiutil create -volname "My Vulkan Application" -srcfolder MyVulkanApp.app -ov -format UDZO MyVulkanApp.dmg +---- + +=== Handling Vulkan Dependencies + +One of the key considerations when packaging Vulkan applications is handling the Vulkan loader and any required extensions. + +==== Vulkan Loader + +The Vulkan loader is the component that connects your application to the Vulkan implementation on the user's system. There are different approaches to handling the loader: + +1. *Rely on System-Installed Loader*: Require users to have the Vulkan SDK or drivers installed +2. *Bundle the Loader*: Include the Vulkan loader with your application +3. *Hybrid Approach*: Check for a system-installed loader and fall back to a bundled one if not found + +Here's an example of a hybrid approach: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class VulkanLoader { +public: + static bool initialize() { + try { + // First, try to use the system-installed Vulkan loader + if (try_system_loader()) { + std::cout << "Using system-installed Vulkan loader" << std::endl; + return true; + } + + // If that fails, try to use the bundled loader + if (try_bundled_loader()) { + std::cout << "Using bundled Vulkan loader" << std::endl; + return true; + } + + // If both approaches fail, report an error + std::cerr << "Failed to initialize Vulkan loader" << std::endl; + return false; + } catch (const std::exception& e) { + std::cerr << "Error initializing Vulkan loader: " << e.what() << std::endl; + return false; + } + } + +private: + static bool try_system_loader() { + try { + // Create a Vulkan instance to test if the system loader works + vk::raii::Context context; + vk::ApplicationInfo app_info{}; + app_info.setApiVersion(VK_API_VERSION_1_2); + + vk::InstanceCreateInfo create_info{}; + create_info.setPApplicationInfo(&app_info); + + vk::raii::Instance instance(context, create_info); + return true; + } catch (...) { + return false; + } + } + + static bool try_bundled_loader() { + try { + // Set the path to the bundled Vulkan loader + #if defined(_WIN32) + std::string loader_path = get_executable_path() + "\\vulkan-1.dll"; + SetDllDirectoryA(get_executable_path().c_str()); + #elif defined(__linux__) + std::string loader_path = get_executable_path() + "/libvulkan.so.1"; + setenv("LD_LIBRARY_PATH", get_executable_path().c_str(), 1); + #elif defined(__APPLE__) + std::string loader_path = get_executable_path() + "/../Frameworks/libMoltenVK.dylib"; + setenv("DYLD_LIBRARY_PATH", (get_executable_path() + "/../Frameworks").c_str(), 1); + #endif + + // Check if the bundled loader exists + if (!std::filesystem::exists(loader_path)) { + return false; + } + + // Try to create a Vulkan instance using the bundled loader + vk::raii::Context context; + vk::ApplicationInfo app_info{}; + app_info.setApiVersion(VK_API_VERSION_1_2); + + vk::InstanceCreateInfo create_info{}; + create_info.setPApplicationInfo(&app_info); + + vk::raii::Instance instance(context, create_info); + return true; + } catch (...) { + return false; + } + } + + static std::string get_executable_path() { + #if defined(_WIN32) + char path[MAX_PATH]; + GetModuleFileNameA(NULL, path, MAX_PATH); + std::string exe_path(path); + return exe_path.substr(0, exe_path.find_last_of("\\/")); + #elif defined(__linux__) + char result[PATH_MAX]; + ssize_t count = readlink("/proc/self/exe", result, PATH_MAX); + std::string exe_path(result, (count > 0) ? count : 0); + return exe_path.substr(0, exe_path.find_last_of("/")); + #elif defined(__APPLE__) + char path[PATH_MAX]; + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0) { + std::string exe_path(path); + return exe_path.substr(0, exe_path.find_last_of("/")); + } + return ""; + #endif + } +}; +---- + +==== Vulkan Layers and Extensions + +If your application requires specific Vulkan layers or extensions, you need to handle them appropriately: + +1. *Document Requirements*: Clearly document which extensions your application requires +2. *Check for Support*: Always check if required extensions are available before using them +3. *Provide Fallbacks*: Implement fallback behavior for missing extensions when possible +4. *Bundle Layers*: For development tools, consider bundling validation layers + +=== Shader Management + +Shaders are a critical part of Vulkan applications, and they need special consideration during packaging: + +1. *Pre-Compile Shaders*: Package pre-compiled SPIR-V shaders rather than GLSL source +2. *Shader Versioning*: Implement a versioning system for shaders to handle updates +3. *Shader Optimization*: Consider optimizing shaders for different hardware targets +4. *Shader Caching*: Implement a shader cache to improve load times + +Here's an example of a shader management system for a packaged application: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class ShaderManager { +public: + ShaderManager(vk::raii::Device& device) : device(device) { + // Determine the shader directory based on the application's location + shader_dir = get_application_directory() + "/shaders"; + + // Create a shader module cache + shader_cache.reserve(100); // Reserve space for up to 100 shader modules + } + + vk::raii::ShaderModule load_shader(const std::string& name) { + // Check if the shader is already in the cache + auto it = shader_cache.find(name); + if (it != shader_cache.end()) { + return vk::raii::ShaderModule(nullptr, nullptr, nullptr); // Return a copy of the cached module + } + + // Load the shader from the package + std::string path = shader_dir + "/" + name + ".spv"; + std::vector code = read_file(path); + + // Create the shader module + vk::ShaderModuleCreateInfo create_info{}; + create_info.setCodeSize(code.size()); + create_info.setPCode(reinterpret_cast(code.data())); + + // Create and cache the shader module + vk::raii::ShaderModule module(device, create_info); + shader_cache[name] = std::move(module); + + return vk::raii::ShaderModule(nullptr, nullptr, nullptr); // Return a copy of the cached module + } + + void clear_cache() { + shader_cache.clear(); + } + +private: + std::string get_application_directory() { + // Platform-specific code to get the application directory + // ... + return "."; // Placeholder + } + + std::vector read_file(const std::string& path) { + std::ifstream file(path, std::ios::ate | std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Failed to open shader file: " + path); + } + + size_t file_size = static_cast(file.tellg()); + std::vector buffer(file_size); + + file.seekg(0); + file.read(buffer.data(), file_size); + file.close(); + + return buffer; + } + + vk::raii::Device& device; + std::string shader_dir; + std::unordered_map shader_cache; +}; +---- + +=== Automated Packaging with CI/CD + +As we discussed in the CI/CD section, automating the packaging process can save time and reduce errors. Here's how to integrate packaging into your CI/CD pipeline: + +1. *Build Matrix*: Set up a build matrix for different platforms and configurations +2. *Packaging Scripts*: Create scripts for each platform's packaging process +3. *Version Management*: Automatically increment version numbers based on git tags or other criteria +4. *Artifact Storage*: Store packaged applications as build artifacts +5. *Release Automation*: Automate the release process to distribution platforms + +Here's an example of a GitHub Actions workflow that includes packaging: + +[source,yaml] +---- +name: Build and Package + +on: + push: + tags: + - 'v*' + +jobs: + build-and-package: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + package-script: ./scripts/package_linux.sh + artifact-name: MyVulkanApp-Linux + artifact-path: MyVulkanApp-x86_64.AppImage + - os: windows-latest + package-script: .\scripts\package_windows.bat + artifact-name: MyVulkanApp-Windows + artifact-path: MyVulkanApp_Installer.exe + - os: macos-latest + package-script: ./scripts/package_macos.sh + artifact-name: MyVulkanApp-macOS + artifact-path: MyVulkanApp.dmg + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Vulkan SDK + uses: humbletim/install-vulkan-sdk@v1.1.1 + with: + version: latest + cache: true + + - name: Configure CMake + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build ${{github.workspace}}/build --config Release + + - name: Package + run: ${{ matrix.package-script }} + + - name: Upload Package + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.artifact-name }} + path: ${{ matrix.artifact-path }} + + create-release: + needs: build-and-package + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v3 + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + MyVulkanApp-Linux/MyVulkanApp-x86_64.AppImage + MyVulkanApp-Windows/MyVulkanApp_Installer.exe + MyVulkanApp-macOS/MyVulkanApp.dmg +---- + +=== Conclusion + +Packaging and distribution are critical steps in the lifecycle of a Vulkan application. By carefully considering platform-specific requirements, handling dependencies appropriately, and automating the packaging process, you can ensure a smooth experience for your users across different platforms. + +Remember that the goal of packaging is to make installation and updates as seamless as possible for your users. Invest time in creating a robust packaging and distribution system, and your users will benefit from a more professional and reliable application. + +In the next and final section, we'll summarize what we've learned throughout this chapter on tooling for Vulkan applications. + +link:05_extensions.adoc[Previous: Vulkan Extensions for Robustness] | link:07_conclusion.adoc[Next: Conclusion] diff --git a/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc b/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc new file mode 100644 index 00000000..91a36b6d --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/07_conclusion.adoc @@ -0,0 +1,232 @@ +:pp: {plus}{plus} + += Tooling: Conclusion + +== Conclusion + +In this chapter, we've explored a comprehensive set of tools and techniques for developing, debugging, and distributing Vulkan applications. Let's summarize what we've learned and discuss how to apply these techniques in your own projects. + +=== What We've Learned + +==== CI/CD for Vulkan Projects + +We started by implementing a continuous integration and continuous deployment pipeline specifically tailored for Vulkan applications. This included: + +* Setting up a basic CI/CD pipeline with GitHub Actions +* Handling Vulkan-specific considerations like SDK installation and GPU availability +* Automating testing for Vulkan applications +* Packaging and distributing Vulkan applications across different platforms + +A well-designed CI/CD pipeline helps ensure consistent quality across builds and platforms, catching issues early in the development process. + +==== Debugging with VK_KHR_debug_utils and RenderDoc + +We then explored powerful debugging tools for Vulkan applications: + +* Using the VK_KHR_debug_utils extension for in-application debugging +* Labeling objects, command buffers, and queue operations for better debugging +* Integrating RenderDoc for frame capture and analysis +* Combining these approaches for comprehensive debugging + +These tools provide visibility into the complex operations happening on the GPU, making it easier to identify and fix issues in your rendering pipeline. + +==== Crash Handling and Minidumps + +Next, we implemented robust crash handling mechanisms: + +* Basic crash handling for exceptions and signals +* Generating minidumps for detailed crash analysis +* Collecting Vulkan-specific information in crash reports +* Integrating with telemetry systems for production applications + +Proper crash handling helps you diagnose and fix issues that occur in production environments, leading to a more stable and reliable application. + +==== Vulkan Extensions for Robustness + +Finally, we explored Vulkan extensions that can help make your application more resilient to undefined behavior: + +* Using VK_EXT_robustness2 for handling out-of-bounds accesses and null descriptors +* Implementing other robustness extensions like VK_KHR_buffer_device_address and VK_EXT_descriptor_indexing +* Combining robustness extensions with debugging tools for maximum effectiveness + +These extensions provide valuable tools for making your application more robust against common errors, though they should not be seen as a substitute for fixing the underlying issues. + +=== Putting It All Together + +Throughout this chapter, we've used modern C++20 modules and the vk::raii namespace for resource management. This approach offers several advantages: + +* Improved code organization with modules +* Automatic resource cleanup with RAII +* More readable and maintainable code +* Better error handling with exceptions + +Let's see how all these components can work together in a complete application: + +[source,cpp] +---- +import std; +import vulkan_raii; + +class VulkanApplication { +public: + VulkanApplication() { + // Initialize crash handler + crash_handler::initialize("MyVulkanApp", "crash_logs"); + + // Initialize Vulkan with debugging and robustness + initialize_vulkan(); + } + + void run() { + // Main application loop + while (!should_close()) { + try { + update(); + render(); + } catch (const vk::SystemError& e) { + // Handle recoverable Vulkan errors + std::cerr << "Vulkan error: " << e.what() << std::endl; + if (!recover_from_error()) { + break; + } + } + } + + cleanup(); + } + +private: + void initialize_vulkan() { + // Create instance with validation layers in debug builds + #ifdef _DEBUG + enable_validation_layers = true; + #else + enable_validation_layers = false; + #endif + + // Create instance + instance = create_instance(); + + // Set up debug messenger if validation is enabled + if (enable_validation_layers) { + debug_messenger = create_debug_messenger(instance); + } + + // Select physical device + physical_device = select_physical_device(instance); + + // Check for robustness support + has_robustness2 = check_robustness2_support(physical_device); + + // Create logical device with robustness if available + device = create_device(physical_device); + + // Name Vulkan objects for debugging + if (enable_validation_layers) { + debug_utils::set_name(device, *device, "Main Device"); + // Name other objects as they're created + } + + // Initialize other Vulkan resources + // ... + } + + void render() { + // Begin frame + auto cmd_buffer = begin_frame(); + + // Label command buffer regions for debugging + if (enable_validation_layers) { + vk::DebugUtilsLabelEXT label_info{}; + label_info.setPLabelName("Main Render Pass"); + label_info.setColor(std::array{0.0f, 1.0f, 0.0f, 1.0f}); + cmd_buffer.beginDebugUtilsLabelEXT(label_info); + } + + // Record rendering commands + // ... + + // End debug label + if (enable_validation_layers) { + cmd_buffer.endDebugUtilsLabelEXT(); + } + + // End frame + end_frame(cmd_buffer); + + // Capture frame with RenderDoc if requested + if (capture_next_frame) { + if (renderdoc_api) { + renderdoc_api->TriggerCapture(); + } + capture_next_frame = false; + } + } + + // Vulkan objects + vk::raii::Context context; + vk::raii::Instance instance{nullptr}; + vk::raii::DebugUtilsMessengerEXT debug_messenger{nullptr}; + vk::raii::PhysicalDevice physical_device{nullptr}; + vk::raii::Device device{nullptr}; + + // Flags + bool enable_validation_layers = false; + bool has_robustness2 = false; + bool capture_next_frame = false; + + // RenderDoc API + RENDERDOC_API_1_4_1* renderdoc_api = nullptr; +}; +---- + +=== Best Practices for Professional Vulkan Development + +Based on what we've covered in this chapter, here are some best practices for professional Vulkan development: + +1. *Automate Your Workflow*: Use CI/CD pipelines to automate building, testing, and packaging your application. + +2. *Debug Early and Often*: Use validation layers and debugging tools throughout development, not just when issues arise. + +3. *Name Your Objects*: Use VK_KHR_debug_utils to give meaningful names to Vulkan objects, making debugging much easier. + +4. *Prepare for Crashes*: Implement robust crash handling and reporting mechanisms from the start of your project. + +5. *Consider Robustness*: Evaluate the trade-offs of using robustness extensions based on your application's needs. + +6. *Test Across Platforms*: Vulkan applications can behave differently across different hardware and drivers, so test extensively. + +7. *Document Your Requirements*: Clearly document which Vulkan extensions and features your application requires. + +8. *Stay Updated*: The Vulkan ecosystem is constantly evolving, so stay informed about new extensions and tools. + +=== Future Directions + +As Vulkan continues to evolve, new tools and techniques will emerge for developing, debugging, and distributing applications. Some areas to watch include: + +* *Improved Debugging Tools*: Tools like RenderDoc continue to add new features for Vulkan debugging. +* *Ray Tracing Tooling*: As ray tracing becomes more common, expect more specialized tools for debugging and optimizing ray tracing pipelines. +* *Machine Learning Integration*: Tools that use machine learning to identify potential issues or optimize performance. +* *Cross-API Development*: Tools that help manage development across multiple graphics APIs (Vulkan, DirectX, Metal). + +=== Final Thoughts + +Developing professional Vulkan applications requires more than just understanding the API—it requires a comprehensive tooling ecosystem that supports the entire development lifecycle. By implementing the tools and techniques covered in this chapter, you'll be well-equipped to develop, debug, and distribute high-quality Vulkan applications. + +Remember that tooling is an investment that pays dividends throughout the development process. Time spent setting up good CI/CD pipelines, debugging tools, and crash handling mechanisms will save you countless hours of troubleshooting and manual work later on. + +=== Code Examples + +The complete code for this chapter can be found in the following files: + +* `simple_engine/32_cicd_setup.cpp`: Implementation of CI/CD for Vulkan projects +* `simple_engine/33_debug_utils.cpp`: Implementation of debugging with VK_KHR_debug_utils and RenderDoc +* `simple_engine/34_crash_handling.cpp`: Implementation of crash handling and minidumps +* `simple_engine/35_robustness_extensions.cpp`: Implementation of Vulkan extensions for robustness + +link:../../attachments/simple_engine/32_cicd_setup.cpp[CI/CD Setup C{pp} code] +link:../../attachments/simple_engine/33_debug_utils.cpp[Debug Utils C{pp} code] +link:../../attachments/simple_engine/34_crash_handling.cpp[Crash Handling C{pp} code] +link:../../attachments/simple_engine/35_robustness_extensions.cpp[Robustness Extensions C{pp} code] + +xref:06_packaging_and_distribution.adoc[Previous: Packaging and Distribution] | xref:../Mobile_Development/01_introduction.adoc[Next: Mobile Development] diff --git a/en/Building_a_Simple_Engine/Tooling/index.adoc b/en/Building_a_Simple_Engine/Tooling/index.adoc new file mode 100644 index 00000000..25d26369 --- /dev/null +++ b/en/Building_a_Simple_Engine/Tooling/index.adoc @@ -0,0 +1,15 @@ +:pp: {plus}{plus} + += Tooling + +This chapter covers essential tooling and techniques for developing, debugging, and distributing Vulkan applications, with a focus on using modern C++20 modules and the vk::raii namespace. + +* xref:01_introduction.adoc[Introduction] +* xref:02_cicd.adoc[CI/CD for Vulkan Projects] +* xref:03_debugging_and_renderdoc.adoc[Debugging with VK_KHR_debug_utils and RenderDoc] +* xref:04_crash_minidump.adoc[Crash Handling and Minidumps] +* xref:05_extensions.adoc[Vulkan Extensions for Robustness] +* xref:06_packaging_and_distribution.adoc[Packaging and Distribution] +* xref:07_conclusion.adoc[Conclusion] + +xref:../Subsystems/06_conclusion.adoc[Previous: Subsystems Conclusion] | link:../index.html[Back to Building a Simple Engine] diff --git a/en/Building_a_Simple_Engine/introduction.adoc b/en/Building_a_Simple_Engine/introduction.adoc new file mode 100644 index 00000000..16c3b90e --- /dev/null +++ b/en/Building_a_Simple_Engine/introduction.adoc @@ -0,0 +1,69 @@ +:pp: {plus}{plus} + += Building a Simple Engine: Introduction + +== Introduction + +Welcome to the "Building a Simple Engine" tutorial series! This series marks a transition from the foundational Vulkan concepts covered in the previous chapters to a more structured approach focused on building a reusable rendering engine. + +=== A New Learning Approach + +While the previous tutorial series focused on introducing individual Vulkan concepts step by step, this series takes a different approach: + +This series targets readers who have completed the xref:../00_Introduction.adoc[Vulkan Tutorial] and feel comfortable with the fundamentals. We’ll emphasize architectural concepts and design patterns over exhaustive API permutations, so you develop an engine mindset rather than a collection of snippets. Expect to do more independent work: fill in smaller gaps, experiment, and lean on the https://docs.vulkan.org/guide/latest/[Vulkan Guide], https://docs.vulkan.org/samples/latest/[Samples], and https://docs.vulkan.org/spec/latest/[Specification] as primary references. If a topic feels too advanced, revisit the original tutorial and return when ready. + +=== What to Expect + +The "Building a Simple Engine" series is designed as a starting point for your journey into engine development, not a finishing point. We'll cover: + +1. *Engine Architecture* - How to structure your code for flexibility, maintainability, and extensibility. + +2. *Resource Management* - More sophisticated approaches to handling models, textures, and other assets. + +3. *Rendering Techniques* - Implementation of modern rendering approaches within an engine framework. + +4. *Performance Considerations* - How to design your engine with performance in mind. + +5. *Publication Considerations* - How to prepare your application for distribution in a professional environment, including packaging, deployment, and platform-specific considerations. + +Throughout this series, we encourage you to experiment, extend the provided examples, and even challenge some of our design decisions. The best way to learn engine development is by doing, and sometimes by making (and learning from) mistakes. + +Throughout our engine implementation, we're using vk::raii dynamic rendering and C++20 modules. The vk::raii namespace provides Resource Acquisition Is Initialization (RAII) wrappers for Vulkan objects, which helps with resource management and makes the code cleaner. Dynamic rendering simplifies the rendering process by eliminating the need for explicit render passes and framebuffers. C++20 modules improve code organization, compilation times, and encapsulation compared to traditional header files. + +=== How to Use This Tutorial + +Each chapter builds on the last to assemble a small but capable engine. Read a chapter end‑to‑end first, then implement; pause to internalize the concepts; and don’t hesitate to revisit the original Vulkan tutorial when you need a refresher. Treat the code as a starting point—experiment and extend it with your own features. + +Let's begin our journey into engine development with these chapters: + +1. xref:Engine_Architecture/01_introduction.adoc[Engine Architecture] - How to structure your code for flexibility, maintainability, and extensibility. +2. xref:Camera_Transformations/01_introduction.adoc[Camera Transformations] - Implementation of camera systems and transformations. +3. xref:Lighting_Materials/01_introduction.adoc[Lighting & Materials] - Basic lighting models and push constants. +4. xref:GUI/01_introduction.adoc[GUI] - Implementation of a graphical user interface using Dear ImGui. +5. xref:Loading_Models/01_introduction.adoc[Loading Models] - More sophisticated approaches to handling models, textures, and other assets. +6. xref:Subsystems/01_introduction.adoc[Subsystems] - Implementation of Audio and Physics subsystems with Vulkan compute capabilities. +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. + +xref:../conclusion.adoc[Previous: Main Tutorial Conclusion] | xref:Engine_Architecture/01_introduction.adoc[Next: Engine Architecture] + + +=== Getting Started with Example Assets + +To follow along with the attachments-based Simple Engine examples and scenes, fetch the Bistro assets locally. + +- Linux/macOS (default target: attachments/simple_engine/Assets/bistro at repository root): ++ + $ cd attachments/simple_engine + $ ./fetch_bistro_assets.sh + +- Windows (default target: attachments\simple_engine\Assets\bistro at repository root): ++ + > cd attachments\simple_engine + > fetch_bistro_assets.bat + +The scripts use SSH (git@github.com:gpx1000/bistro.git) and fall back to HTTPS if SSH is unavailable. If Git LFS is installed, large files will be pulled automatically. + +Next, take advantage of the install_dependencies_* scripts to ensure you have all necessary dependencies. + +Once assets are available and dependencies are ready, build and run the Simple Engine examples under attachments/simple_engine. See the later chapters for details on scene loading and subsystems referenced by the example code. diff --git a/en/conclusion.adoc b/en/conclusion.adoc new file mode 100644 index 00000000..5608fe28 --- /dev/null +++ b/en/conclusion.adoc @@ -0,0 +1,72 @@ +:pp: {plus}{plus} + += Conclusion +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: left +:icons: font +:source-highlighter: highlightjs +:source-language: c{pp} + +== Conclusion + +Congratulations on completing the Core Vulkan tutorial series! You've built a + solid foundation in Vulkan development that will serve you well in your graphics programming journey. + +=== What You've Learned + +Throughout this tutorial series, you've gained knowledge and practical experience in: + +1. *Vulkan Fundamentals* - Understanding the core concepts of Vulkan, its architecture, and how it differs from other graphics APIs. + +2. *Setting Up a Vulkan Application* - Creating instances, selecting physical devices, creating logical devices, and establishing the rendering pipeline. + +3. *Drawing Operations* - Rendering triangles, working with vertex buffers, and understanding the Vulkan rendering process. + +4. *Advanced Rendering Techniques* - Implementing depth buffering, texture mapping, mipmaps, and multisampling. + +5. *Asset Management* - Loading 3D models and textures for use in your Vulkan applications. + +6. *Performance Optimization* - Using compute shaders and multithreading to improve application performance. + +7. *Ecosystem Integration* - Working with utilities, ensuring compatibility, and understanding Vulkan profiles. + +8. *Platform-Specific Development* - Adapting your Vulkan application for Android. + +9. *Modern Graphics Techniques* - Migrating to glTF and KTX2 formats for improved asset management. + +=== Where to Go From Here + +This tutorial series has provided you with the essential tools for more +advanced Vulkan development. With these fundamentals mastered, you're now ready to explore more complex topics and build more sophisticated applications. + +Future tutorial series will build upon these fundamentals to help you create +more structured and reusable rendering solutions. Future, planned +tutorials will assume you've completed all the chapters in this series and +will guide you through implementing more sophisticated rendering techniques +and architectures. + +Remember that Vulkan development is a continuous learning process. The graphics programming landscape is constantly evolving, and there's always more to learn and explore. + +=== Community Resources and Getting Help + +As you continue your Vulkan journey, you may encounter challenges or have questions. Fortunately, there are several active communities where you can get help: + +1. *Khronos Slack* - Join the official Khronos Group Slack workspace and the #vulkan channel for direct interaction with Vulkan developers and experts. You can get an invitation at https://khr.io/slack[khr.io/slack]. + +2. *Vulkan Discord* - The community-run Vulkan Discord server is a great place for real-time discussions, troubleshooting, and connecting with other Vulkan developers. Join at https://discord.gg/vulkan[discord.gg/vulkan]. + +3. *Reddit* - The r/vulkan subreddit (https://www.reddit.com/r/vulkan/[reddit.com/r/vulkan]) is an active community for sharing news, asking questions, and discussing Vulkan development. + +4. *Stack Overflow* - For specific programming questions, use the +https://stackoverflow.com/questions/tagged/vulkan[vulkan] tag on Stack Overflow. + +5. *Vulkan Specification* - When in doubt, refer to the official https://docs.vulkan.org/spec/latest/[Vulkan Specification] for authoritative information. + +Don't hesitate to reach out to these communities - they're filled with developers who are passionate about Vulkan and eager to help others succeed. + +Thank you for following along with this tutorial series. You've taken a big +first step in a long journey. + +link:17_Multithreading.adoc[Previous: Multithreading] | link:Building_a_Simple_Engine/introduction.adoc[Next: Building a Simple Engine] diff --git a/images/bistro.png b/images/bistro.png new file mode 100644 index 00000000..ca248e99 Binary files /dev/null and b/images/bistro.png differ diff --git a/images/component_based_architecture_diagram.png b/images/component_based_architecture_diagram.png new file mode 100644 index 00000000..b1f089e2 Binary files /dev/null and b/images/component_based_architecture_diagram.png differ diff --git a/images/data_oriented_design_diagram.svg b/images/data_oriented_design_diagram.svg new file mode 100644 index 00000000..d0611425 --- /dev/null +++ b/images/data_oriented_design_diagram.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + Data-Oriented Design + + + Object-Oriented Approach + + + + GameObject 1 + position: (10, 20, 30) + rotation: (0, 45, 0) + scale: (1, 1, 1) + + + GameObject 2 + position: (50, 0, 10) + rotation: (90, 0, 0) + scale: (2, 2, 2) + + + GameObject 3 + position: (0, 10, 50) + rotation: (0, 180, 0) + scale: (1, 3, 1) + + + VS + + + Data-Oriented Approach + + + + Positions Array + [10,20,30, 50,0,10, 0,10,50, ...] + + + Rotations Array + [0,45,0, 90,0,0, 0,180,0, ...] + + + Scales Array + [1,1,1, 2,2,2, 1,3,1, ...] + + + + Transform System + + + Physics System + + + + + + + + Objects with mixed data types + Contiguous arrays of same data type + diff --git a/images/layered_architecture_diagram.png b/images/layered_architecture_diagram.png new file mode 100644 index 00000000..c5b0d486 Binary files /dev/null and b/images/layered_architecture_diagram.png differ diff --git a/images/matrix-order-comparison.svg b/images/matrix-order-comparison.svg new file mode 100644 index 00000000..68c26ef0 --- /dev/null +++ b/images/matrix-order-comparison.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + Matrix Order Comparison: T × R × S vs R × T × S + + + CORRECT ORDER: T × R × S (Applied right-to-left: Scale → Rotate → Translate) + + + + Step 1: Scale + + Original cube + at origin + + + + + + + + Step 2: Rotate + + Rotated in place + around its center + + + + + + + + Step 3: Translate + + Moved to new position + (final desired result) + + + Origin + + + + + + + INCORRECT ORDER: R × T × S (Applied right-to-left: Scale → Translate → Rotate) + + + + Step 1: Scale + + Original cube + at origin + + + + + + + + Step 2: Translate + + Moved away from + origin first + + + Origin + + + + + + + + Step 3: Rotate + + Rotated around world origin + (cube orbits, not desired!) + + + Origin + + + + + + + + Key Insight: + In the incorrect order, the cube is moved away from the origin first, then rotated around the world origin, + causing it to orbit in a circle rather than rotate in place and then move to the desired position. + + diff --git a/images/rendering_pipeline_flowchart.png b/images/rendering_pipeline_flowchart.png new file mode 100644 index 00000000..8a7614e2 Binary files /dev/null and b/images/rendering_pipeline_flowchart.png differ diff --git a/images/service_locator_pattern_diagram.svg b/images/service_locator_pattern_diagram.svg new file mode 100644 index 00000000..29ced464 --- /dev/null +++ b/images/service_locator_pattern_diagram.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + Service Locator Pattern + + + + IAudioService + + PlaySound(soundName) + + + + OpenALAudioService + implements IAudioService + + + NullAudioService + implements IAudioService + + + ... + Other services + + + + ServiceLocator + + GetAudioService() + + ProvideAudioService(service) + + + + Client Code + + + + + + + + + + + + + + + + + + + Concrete Implementations + Service Interface + Global Access Point + Uses services via locator +