[
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: Linux/Windows Build\nrun-name: ${{ github.actor }} is building splatapult\non: [push]\njobs:\n  build-linux:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          submodules: true\n      - name: Install System Packages\n        uses: ConorMacBride/install-package@v1\n        with:\n          apt: libxmu-dev libxi-dev libgl-dev libxcb-glx0-dev libglu1-mesa-dev libxxf86vm-dev libxrandr-dev\n      - name: Install CMake\n        uses: lukka/get-cmake@v3.29.0\n      - name: Create Build Dir\n        run: mkdir build\n      - name: CMake Configure\n        working-directory: build\n        run: cmake -DSHIPPING=ON -DCMAKE_TOOLCHAIN_FILE=\"../vcpkg/scripts/buildsystems/vcpkg.cmake\" ..\n      - name: CMake Build\n        working-directory: build\n        run: cmake --build . --config=Release\n  build-windows:\n    runs-on: windows-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          submodules: true\n      - name: Get Microsoft Visual Studio for C++\n        uses: ilammy/msvc-dev-cmd@v1\n      - name: Install CMake\n        uses: lukka/get-cmake@v3.29.0\n      - name: Create Build Dir\n        run: mkdir build\n      - name: CMake Configure\n        working-directory: build\n        run: cmake -DSHIPPING=ON -DCMAKE_TOOLCHAIN_FILE=\"../vcpkg/scripts/buildsystems/vcpkg.cmake\" ..\n      - name: CMake Build\n        working-directory: build\n        run: cmake --build . --config=Release\n      - name: Upload Artifiacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: windows-build\n          path: build/Release\n"
  },
  {
    "path": ".gitignore",
    "content": "build/\nmeta-quest/ovr_openxr_mobile_sdk_59.0/\nmeta-quest/ovr_openxr_mobile_sdk_59.0.zip\ndata/*\n!data/test.ply\n!data/test_vr.json\nvcpkg_installed/\n\n# ignore archived release zip\nsplatapult*.zip\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"vcpkg\"]\n\tpath = vcpkg\n\turl = https://github.com/microsoft/vcpkg\n"
  },
  {
    "path": "BUILD.md",
    "content": "Windows Build (vcpkg submodule)\n-----------------------\n* Install Visual Studio 2022\n* Install cmake 3.27.1\n* Ensure splatapult has the vcpkg submodule.\n  Either clone with the --recursive flag so that the vcpkg submodule is added\n  or execute `git submodule init` and `git submodule update` after a regular clone.\n* Bootstrap vcpkg\n  - `cd vcpkg`\n  - `bootstrap-vcpg.bat`\n* Execute cmake to create a Visual Studio solution file\n  - `mkdir build`\n  - `cd build`\n  - `cmake .. -G \"Visual Studio 17 2022\" -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake`\n* Build exe by using Visual Studio by loading splatapult.sln or from the command line:\n  - `cmake --build . --config=Release`\n\nWindows Shipping Builds\n-------------------------\nThe SHIPPING cmake option is used to create a release version of splataplut.\nA post build step will copy all of the the data folders (font, shader, texture) into the build directory.\nAnd the resulting exe will use that copy.  You can then zip up the folder and distrubute it to users.\n\n* To create a shipping build:\n    - `mkdir build`\n    - `cd build`\n    - `cmake .. -G \"Visual Studio 17 2022\" -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake -DSHIPPING=ON`\n    - `cmake --build . --config=Release`\n\nLinux Build (vcpkg submodule)\n--------------------\n* Install dependencies\n  - `sudo apt-get install clang`\n  - `sudo apt-get install cmake`\n  - `sudo apt-get install freeglut3-dev`\n  - `sudo apt-get install libopenxr-dev`\n* Ensure splatapult has the vcpkg submodule.\n  Either clone with the --recursive flag so that the vcpkg submodule is added\n  or execute `git submodule init` and `git submodule update` after a regular clone.\n* Bootstrap vcpkg\n  - `cd vcpkg`\n  - `bootstrap-vcpg.sh`\n* Execute cmake to create a Makefile\n  - `mkdir build`\n  - `cd build`\n  - `cmake .. -G \"Unix Makefiles\" -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake`\n* build executable\n  - `cmake --build . --config=Release`\n\n*EXPERIMENTAL* Meta Quest Build (OUT OF DATE)\n--------------------\nNOTE: Although the quest build functions it is much to slow for most scenes.\nA Quest2 headset can only run a scene consisting of 25k splats.\n\n* Use vcpkg to install the following packages:\n    - `vcpkg install glm:arm64-android`\n    - `vcpkg install libpng:arm64-android`\n    - `vcpkg install nlohmann-json:arm64-android`\n* Set the following environment var ANDROID_VCPKG_DIR to point to vcpkg/installed/arm64-android.\n* Download the [Meta OpenXR Mobile SDK 59.0](https://developer.oculus.com/downloads/package/oculus-openxr-mobile-sdk/)\n* Install [Android Studio Bumble Bee, Patch 3](https://developer.android.com/studio/archive)\n  newer versions do not work with Meta OpenXR Mobile SDK 59.0.\n  Follow this guide to setup [Android Studio](https://developer.oculus.com/documentation/native/android/mobile-studio-setup-android/)\n* Copy the ovr_openxr_mobile_sdk_59.0 dir into the meta-quest dir.\n* Copy the meta-quest/splatapult dir to ovr_openxr_mobile_sdk_59.0/XrSamples/splataplut\n* Open the ovr_openxr_mobile_sdk_59.0/XrSamples/splatapult in AndroidStudio.\n* Sync and Build\n\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "# See BUILD.md for more info\n# Windows cheat sheet\n# > mkdir build\n# > cd build\n# > cmake -DSHIPPING=ON -DCMAKE_TOOLCHAIN_FILE=\"C:\\msys64\\home\\hyperlogic\\code\\vcpkg\\scripts\\buildsystems\\vcpkg.cmake\" ..\n# > cmake --build . --config Release\n\ncmake_minimum_required(VERSION 3.13 FATAL_ERROR)\nset(PROJECT_NAME splatapult)\nproject(${PROJECT_NAME} LANGUAGES CXX)\n\noption(SHIPPING \"Build for shipping\" OFF)\n\nif(UNIX)\n    find_package(X11 REQUIRED)\nendif()\n\nif(WIN32)\n    # kind of a hack, I want to be able to include glm/glm.hpp on windows and linux\n    include_directories(${VCPKG_INSTALLED_DIR}/x64-windows/include)\nendif()\n\n# opengl\nfind_package(OpenGL REQUIRED)\ninclude_directories(${GL_INCLUDE_DIRS})\n\n# sdl2\nfind_package(SDL2 CONFIG REQUIRED)\n\n# glew\nfind_package(GLEW REQUIRED)\n\n# glm\nfind_package(glm REQUIRED)\n\n# png\nfind_package(PNG REQUIRED)\n\n# nlohmann-json\nif (WIN32)\n    find_package(nlohmann_json CONFIG REQUIRED)\nendif()\n\n# eigen\nfind_package(Eigen3 CONFIG REQUIRED)\n\n# tracy\nif(WIN32 AND NOT SHIPPING)\n    find_package(Tracy CONFIG REQUIRED)\n    add_compile_definitions(TRACY_ENABLE)\n    set(TRACY_LIBRARIES, Tracy::TracyClient)\nelse()\n    set(TRACY_LIBRARIES, \"\")\nendif()\n\n# openxr-loader\nfind_package(OpenXR CONFIG REQUIRED)\n\n# src\ninclude_directories(src)\nadd_executable(${PROJECT_NAME}\n    src/core/binaryattribute.cpp\n    src/core/debugrenderer.cpp\n    src/core/framebuffer.cpp\n    src/core/image.cpp\n    src/core/inputbuddy.cpp\n    src/core/log.cpp\n    src/core/program.cpp\n    src/core/texture.cpp\n    src/core/util.cpp\n    src/core/vertexbuffer.cpp\n    src/core/textrenderer.cpp\n    src/core/xrbuddy.cpp\n\n    src/app.cpp\n    src/camerasconfig.cpp\n    src/camerapathrenderer.cpp\n    src/flycam.cpp\n    src/gaussiancloud.cpp\n    src/magiccarpet.cpp\n    src/ply.cpp\n    src/pointcloud.cpp\n    src/pointrenderer.cpp\n    src/sdl_main.cpp\n    src/splatrenderer.cpp\n    src/vrconfig.cpp\n)\n\ntarget_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)\n\nif(WIN32)\n    # Comment this out to see SDL_Log output for debugging\n    # set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS /SUBSYSTEM:WINDOWS)\nendif()\n\nif(WIN32)\n    if(SHIPPING)\n        target_link_libraries(${PROJECT_NAME} PRIVATE\n            ${OPENGL_LIBRARIES}\n            $<TARGET_NAME_IF_EXISTS:SDL2::SDL2main>\n            $<IF:$<TARGET_EXISTS:SDL2::SDL2>,SDL2::SDL2,SDL2::SDL2-static>\n            GLEW::GLEW\n            glm::glm\n            PNG::PNG\n            nlohmann_json::nlohmann_json\n            Eigen3::Eigen\n            OpenXR::headers\n            OpenXR::openxr_loader\n        )\n    else()\n        target_link_libraries(${PROJECT_NAME} PRIVATE\n            ${OPENGL_LIBRARIES}\n            $<TARGET_NAME_IF_EXISTS:SDL2::SDL2main>\n            $<IF:$<TARGET_EXISTS:SDL2::SDL2>,SDL2::SDL2,SDL2::SDL2-static>\n            GLEW::GLEW\n            glm::glm\n            PNG::PNG\n            nlohmann_json::nlohmann_json\n            Eigen3::Eigen\n            Tracy::TracyClient\n            OpenXR::headers\n            OpenXR::openxr_loader\n        )\n    endif()\nelse()\n    target_link_libraries(${PROJECT_NAME} PRIVATE\n        ${OPENGL_LIBRARIES}\n        $<TARGET_NAME_IF_EXISTS:SDL2::SDL2main>\n        $<IF:$<TARGET_EXISTS:SDL2::SDL2>,SDL2::SDL2,SDL2::SDL2-static>\n        GLEW::GLEW\n        glm::glm\n        PNG::PNG\n        # nlohmann_json::nlohmann_json\n        Eigen3::Eigen\n        OpenXR::headers\n        OpenXR::openxr_loader\n        ${X11_LIBRARIES}\n    )\nendif()\n\nif(SHIPPING)\n    add_compile_definitions(SHIPPING)\n\n    # Copy required data directories to the executable directory\n    add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD\n        COMMAND ${CMAKE_COMMAND} -E copy_directory\n                ${CMAKE_SOURCE_DIR}/font\n                $<TARGET_FILE_DIR:${PROJECT_NAME}>/font\n        COMMAND ${CMAKE_COMMAND} -E copy_directory\n                ${CMAKE_SOURCE_DIR}/shader\n                $<TARGET_FILE_DIR:${PROJECT_NAME}>/shader\n        COMMAND ${CMAKE_COMMAND} -E copy_directory\n                ${CMAKE_SOURCE_DIR}/texture\n                $<TARGET_FILE_DIR:${PROJECT_NAME}>/texture\n        COMMAND ${CMAKE_COMMAND} -E copy\n                ${CMAKE_SOURCE_DIR}/data/test.ply\n                $<TARGET_FILE_DIR:${PROJECT_NAME}>/data/test.ply\n    )\nendif()"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright 2024 Anthony J. Thibault\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\nSplatapult\n----------------------------------------------\n![Splatapult logo](/splatapult.png)\n\n[<img src=\"https://i.ytimg.com/vi/18DuNJRZbzQ/maxresdefault.jpg\" width=\"50%\">](https://www.youtube.com/watch?v=18DuNJRZbzQ \"Splatapult Demo\")\n\n![Splatapult Build](https://github.com/hyperlogic/splatapult/actions/workflows/build.yaml/badge.svg)\n\nA program to display 3d gaussian splats files\n\nsplatapult [OPTIONS] ply_filename\n\nOptions\n-------------\n-v, --openxr\n    launch app in vr mode, using openxr runtime\n\n-f, --fullscreen\n    launch window in fullscreen\n\n-d, --debug\n    enable debug logging\n\n--fp16\n    Use 16-bit half-precision floating frame buffer, to reduce color banding artifacts\n\n--fp32\n    Use 32-bit floating point frame buffer, to reduce color banding even more\n\n--nosh\n    Don't load/render full sh, this will reduce memory usage and higher performance\n\n-h, --help\n    show help\n\nDesktop Controls\n--------------------\n* wasd - move\n* q, e - roll\n* t, g - up, down\n* arrow keys - look\n* right mouse button - hold down for mouse look.\n* gamepad - if present, right stick to rotate, left stick to move, bumpers to roll\n* c - toggle between initial SfM point cloud (if present) and gaussian splats.\n* n - jump to next camera\n* p - jump to previous camera\n* y - toggle rendering of camera frustums\n* h - toggle rendering of camera path\n* return - save the current position and orientation of the world into a vr.json file.\n\nVR Controls\n---------------\n* left stick - move\n* right stick - snap turn\n* f - show hide floor carpet.\n* single grab - translate the world.\n* double grab - rotate and translate the world.\n* triple grab - (double grab while trigger is depressed) scale, rotate and translate the world.\n* c - toggle between initial SfM point cloud (if present) and gaussian splats.\n* y - toggle rendering of camera frustums\n* h - toggle rendering of camera path\n* return - save the current position and orientation/scale of the world into a vr.json file.\n\nConfig Files\n----------------------\nIf a \"_vr.json\" file is found, it will be used to determine the proper starting position, scale and orienation for vr mode.\nYou can create your own _vr.json file by manipulating the scene via grab in vr mode, then press return to save.\n\nSplatapult supports the same dir structure that [Gaussian Splatting](https://github.com/graphdeco-inria/gaussian-splatting) code will output.\nWhich is as follows:\n\n```\ndir/\n    point_cloud/\n        iteration_30000/\n            point_cloud.ply\n            point_cloud_vr.json\n    input.ply\n    cameras.json\n```\n\ninput.ply contains the point cloud and cameras.json will contain the camera orientations from the SfM stage.\n\nIf the \"cameras.json\" file is found in the same dir as the ply_filename or it's parent dirs, it will be loaded.\nThe 'n' and 'p' keys can then be used to cycle thru the camera viewpoints.\n\nIt will also support files downloaded from lumalabs.ai, but in this case there will be no point clouds or cameras.\n\n```\ndir/\n    mycapture.ply\n    mycapture_vr.json\n```\nBuild Info\n--------------------\nSee [BUILD.md](BUILD.md) for information on building Splatapult from source.\n\n"
  },
  {
    "path": "data/test_vr.json",
    "content": "{\n    \"floorMat\": [[-0.705639, -0.312374, -0.636, -0.573335], [-1.64858e-05, 0.897588, -0.440836, -1.78893], [0.708571, -0.311061, -0.633378, -0.512137], [0, 0, 0, 1]]\n}"
  },
  {
    "path": "font/JetBrainsMono-Medium.json",
    "content": "{\n    \"texture_width\": 1024,\n    \"glyph_metrics\": {\n        \"32\": {\n            \"ascii_index\": 32,\n            \"xy_lower_left\": [-0.039216, -0.039216],\n            \"xy_upper_right\": [0.039216, 0.039216],\n            \"uv_lower_left\": [0.000000, 0.992188],\n            \"uv_upper_right\": [0.007812, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"33\": {\n            \"ascii_index\": 33,\n            \"xy_lower_left\": [0.122075, -0.047280],\n            \"xy_upper_right\": [0.329538, 0.595667],\n            \"uv_lower_left\": [0.099609, 0.923828],\n            \"uv_upper_right\": [0.123047, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"34\": {\n            \"ascii_index\": 34,\n            \"xy_lower_left\": [0.057559, 0.291429],\n            \"xy_upper_right\": [0.394054, 0.595667],\n            \"uv_lower_left\": [0.199219, 0.964844],\n            \"uv_upper_right\": [0.238281, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"35\": {\n            \"ascii_index\": 35,\n            \"xy_lower_left\": [-0.015022, -0.039216],\n            \"xy_upper_right\": [0.474700, 0.595667],\n            \"uv_lower_left\": [0.298828, 0.924805],\n            \"uv_upper_right\": [0.356445, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"36\": {\n            \"ascii_index\": 36,\n            \"xy_lower_left\": [0.009171, -0.144054],\n            \"xy_upper_right\": [0.450506, 0.708570],\n            \"uv_lower_left\": [0.398438, 0.898438],\n            \"uv_upper_right\": [0.450195, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"37\": {\n            \"ascii_index\": 37,\n            \"xy_lower_left\": [-0.039216, -0.047280],\n            \"xy_upper_right\": [0.490829, 0.595667],\n            \"uv_lower_left\": [0.498047, 0.923828],\n            \"uv_upper_right\": [0.560547, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"38\": {\n            \"ascii_index\": 38,\n            \"xy_lower_left\": [-0.023087, -0.047280],\n            \"xy_upper_right\": [0.506958, 0.603732],\n            \"uv_lower_left\": [0.597656, 0.922852],\n            \"uv_upper_right\": [0.660156, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"39\": {\n            \"ascii_index\": 39,\n            \"xy_lower_left\": [0.146268, 0.291429],\n            \"xy_upper_right\": [0.313409, 0.595667],\n            \"uv_lower_left\": [0.697266, 0.964844],\n            \"uv_upper_right\": [0.715820, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"40\": {\n            \"ascii_index\": 40,\n            \"xy_lower_left\": [0.097881, -0.135990],\n            \"xy_upper_right\": [0.410183, 0.676312],\n            \"uv_lower_left\": [0.796875, 0.903320],\n            \"uv_upper_right\": [0.833008, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"41\": {\n            \"ascii_index\": 41,\n            \"xy_lower_left\": [0.041429, -0.135990],\n            \"xy_upper_right\": [0.361796, 0.676312],\n            \"uv_lower_left\": [0.896484, 0.903320],\n            \"uv_upper_right\": [0.933594, 1.000000],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"42\": {\n            \"ascii_index\": 42,\n            \"xy_lower_left\": [-0.015022, 0.033365],\n            \"xy_upper_right\": [0.474700, 0.515022],\n            \"uv_lower_left\": [0.000000, 0.843750],\n            \"uv_upper_right\": [0.057617, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"43\": {\n            \"ascii_index\": 43,\n            \"xy_lower_left\": [0.009171, 0.033365],\n            \"xy_upper_right\": [0.450506, 0.474700],\n            \"uv_lower_left\": [0.099609, 0.848633],\n            \"uv_upper_right\": [0.151367, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"44\": {\n            \"ascii_index\": 44,\n            \"xy_lower_left\": [0.114010, -0.160183],\n            \"xy_upper_right\": [0.329538, 0.152119],\n            \"uv_lower_left\": [0.199219, 0.864258],\n            \"uv_upper_right\": [0.223633, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"45\": {\n            \"ascii_index\": 45,\n            \"xy_lower_left\": [0.065623, 0.178526],\n            \"xy_upper_right\": [0.394054, 0.329538],\n            \"uv_lower_left\": [0.298828, 0.883789],\n            \"uv_upper_right\": [0.336914, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"46\": {\n            \"ascii_index\": 46,\n            \"xy_lower_left\": [0.122075, -0.047280],\n            \"xy_upper_right\": [0.337603, 0.160183],\n            \"uv_lower_left\": [0.398438, 0.876953],\n            \"uv_upper_right\": [0.422852, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"47\": {\n            \"ascii_index\": 47,\n            \"xy_lower_left\": [0.009171, -0.119861],\n            \"xy_upper_right\": [0.442441, 0.676312],\n            \"uv_lower_left\": [0.498047, 0.805664],\n            \"uv_upper_right\": [0.548828, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"48\": {\n            \"ascii_index\": 48,\n            \"xy_lower_left\": [0.017236, -0.047280],\n            \"xy_upper_right\": [0.442441, 0.603732],\n            \"uv_lower_left\": [0.597656, 0.823242],\n            \"uv_upper_right\": [0.647461, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"49\": {\n            \"ascii_index\": 49,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.595667],\n            \"uv_lower_left\": [0.697266, 0.825195],\n            \"uv_upper_right\": [0.748047, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"50\": {\n            \"ascii_index\": 50,\n            \"xy_lower_left\": [0.009171, -0.039216],\n            \"xy_upper_right\": [0.442441, 0.603732],\n            \"uv_lower_left\": [0.796875, 0.824219],\n            \"uv_upper_right\": [0.847656, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"51\": {\n            \"ascii_index\": 51,\n            \"xy_lower_left\": [0.009171, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.896484, 0.824219],\n            \"uv_upper_right\": [0.946289, 0.900391],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"52\": {\n            \"ascii_index\": 52,\n            \"xy_lower_left\": [0.009171, -0.039216],\n            \"xy_upper_right\": [0.418248, 0.595667],\n            \"uv_lower_left\": [0.000000, 0.725586],\n            \"uv_upper_right\": [0.047852, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"53\": {\n            \"ascii_index\": 53,\n            \"xy_lower_left\": [0.009171, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.099609, 0.724609],\n            \"uv_upper_right\": [0.149414, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"54\": {\n            \"ascii_index\": 54,\n            \"xy_lower_left\": [0.001107, -0.047280],\n            \"xy_upper_right\": [0.450506, 0.595667],\n            \"uv_lower_left\": [0.199219, 0.724609],\n            \"uv_upper_right\": [0.251953, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"55\": {\n            \"ascii_index\": 55,\n            \"xy_lower_left\": [0.017236, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.595667],\n            \"uv_lower_left\": [0.298828, 0.725586],\n            \"uv_upper_right\": [0.350586, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"56\": {\n            \"ascii_index\": 56,\n            \"xy_lower_left\": [0.001107, -0.047280],\n            \"xy_upper_right\": [0.450506, 0.603732],\n            \"uv_lower_left\": [0.398438, 0.723633],\n            \"uv_upper_right\": [0.451172, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"57\": {\n            \"ascii_index\": 57,\n            \"xy_lower_left\": [0.001107, -0.039216],\n            \"xy_upper_right\": [0.450506, 0.603732],\n            \"uv_lower_left\": [0.498047, 0.724609],\n            \"uv_upper_right\": [0.550781, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"58\": {\n            \"ascii_index\": 58,\n            \"xy_lower_left\": [0.122075, -0.047280],\n            \"xy_upper_right\": [0.337603, 0.466635],\n            \"uv_lower_left\": [0.597656, 0.740234],\n            \"uv_upper_right\": [0.622070, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"59\": {\n            \"ascii_index\": 59,\n            \"xy_lower_left\": [0.105946, -0.168248],\n            \"xy_upper_right\": [0.337603, 0.466635],\n            \"uv_lower_left\": [0.697266, 0.725586],\n            \"uv_upper_right\": [0.723633, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"60\": {\n            \"ascii_index\": 60,\n            \"xy_lower_left\": [0.017236, 0.001107],\n            \"xy_upper_right\": [0.434377, 0.498893],\n            \"uv_lower_left\": [0.796875, 0.742188],\n            \"uv_upper_right\": [0.845703, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"61\": {\n            \"ascii_index\": 61,\n            \"xy_lower_left\": [0.017236, 0.089817],\n            \"xy_upper_right\": [0.434377, 0.426312],\n            \"uv_lower_left\": [0.896484, 0.761719],\n            \"uv_upper_right\": [0.945312, 0.800781],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"62\": {\n            \"ascii_index\": 62,\n            \"xy_lower_left\": [0.017236, 0.001107],\n            \"xy_upper_right\": [0.434377, 0.498893],\n            \"uv_lower_left\": [0.000000, 0.642578],\n            \"uv_upper_right\": [0.048828, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"63\": {\n            \"ascii_index\": 63,\n            \"xy_lower_left\": [0.049494, -0.047280],\n            \"xy_upper_right\": [0.410183, 0.595667],\n            \"uv_lower_left\": [0.099609, 0.625000],\n            \"uv_upper_right\": [0.141602, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"64\": {\n            \"ascii_index\": 64,\n            \"xy_lower_left\": [-0.015022, -0.176312],\n            \"xy_upper_right\": [0.466635, 0.603732],\n            \"uv_lower_left\": [0.199219, 0.608398],\n            \"uv_upper_right\": [0.255859, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"65\": {\n            \"ascii_index\": 65,\n            \"xy_lower_left\": [-0.006958, -0.039216],\n            \"xy_upper_right\": [0.466635, 0.595667],\n            \"uv_lower_left\": [0.298828, 0.625977],\n            \"uv_upper_right\": [0.354492, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"66\": {\n            \"ascii_index\": 66,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.450506, 0.595667],\n            \"uv_lower_left\": [0.398438, 0.625977],\n            \"uv_upper_right\": [0.448242, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"67\": {\n            \"ascii_index\": 67,\n            \"xy_lower_left\": [0.025300, -0.047280],\n            \"xy_upper_right\": [0.442441, 0.603732],\n            \"uv_lower_left\": [0.498047, 0.624023],\n            \"uv_upper_right\": [0.546875, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"68\": {\n            \"ascii_index\": 68,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.597656, 0.625977],\n            \"uv_upper_right\": [0.645508, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"69\": {\n            \"ascii_index\": 69,\n            \"xy_lower_left\": [0.033365, -0.039216],\n            \"xy_upper_right\": [0.442441, 0.595667],\n            \"uv_lower_left\": [0.697266, 0.625977],\n            \"uv_upper_right\": [0.745117, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"70\": {\n            \"ascii_index\": 70,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.442441, 0.595667],\n            \"uv_lower_left\": [0.796875, 0.625977],\n            \"uv_upper_right\": [0.845703, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"71\": {\n            \"ascii_index\": 71,\n            \"xy_lower_left\": [0.017236, -0.047280],\n            \"xy_upper_right\": [0.442441, 0.603732],\n            \"uv_lower_left\": [0.896484, 0.624023],\n            \"uv_upper_right\": [0.946289, 0.701172],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"72\": {\n            \"ascii_index\": 72,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.000000, 0.526367],\n            \"uv_upper_right\": [0.047852, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"73\": {\n            \"ascii_index\": 73,\n            \"xy_lower_left\": [0.033365, -0.039216],\n            \"xy_upper_right\": [0.418248, 0.595667],\n            \"uv_lower_left\": [0.099609, 0.526367],\n            \"uv_upper_right\": [0.144531, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"74\": {\n            \"ascii_index\": 74,\n            \"xy_lower_left\": [-0.015022, -0.047280],\n            \"xy_upper_right\": [0.418248, 0.595667],\n            \"uv_lower_left\": [0.199219, 0.525391],\n            \"uv_upper_right\": [0.250000, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"75\": {\n            \"ascii_index\": 75,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.466635, 0.595667],\n            \"uv_lower_left\": [0.298828, 0.526367],\n            \"uv_upper_right\": [0.350586, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"76\": {\n            \"ascii_index\": 76,\n            \"xy_lower_left\": [0.057559, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.595667],\n            \"uv_lower_left\": [0.398438, 0.526367],\n            \"uv_upper_right\": [0.445312, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"77\": {\n            \"ascii_index\": 77,\n            \"xy_lower_left\": [0.009171, -0.039216],\n            \"xy_upper_right\": [0.450506, 0.595667],\n            \"uv_lower_left\": [0.498047, 0.526367],\n            \"uv_upper_right\": [0.549805, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"78\": {\n            \"ascii_index\": 78,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.597656, 0.526367],\n            \"uv_upper_right\": [0.645508, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"79\": {\n            \"ascii_index\": 79,\n            \"xy_lower_left\": [0.017236, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.603732],\n            \"uv_lower_left\": [0.697266, 0.524414],\n            \"uv_upper_right\": [0.746094, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"80\": {\n            \"ascii_index\": 80,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.595667],\n            \"uv_lower_left\": [0.796875, 0.526367],\n            \"uv_upper_right\": [0.847656, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"81\": {\n            \"ascii_index\": 81,\n            \"xy_lower_left\": [0.017236, -0.176312],\n            \"xy_upper_right\": [0.442441, 0.603732],\n            \"uv_lower_left\": [0.896484, 0.508789],\n            \"uv_upper_right\": [0.946289, 0.601562],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"82\": {\n            \"ascii_index\": 82,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.595667],\n            \"uv_lower_left\": [0.000000, 0.426758],\n            \"uv_upper_right\": [0.050781, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"83\": {\n            \"ascii_index\": 83,\n            \"xy_lower_left\": [0.009171, -0.047280],\n            \"xy_upper_right\": [0.450506, 0.603732],\n            \"uv_lower_left\": [0.099609, 0.424805],\n            \"uv_upper_right\": [0.151367, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"84\": {\n            \"ascii_index\": 84,\n            \"xy_lower_left\": [-0.006958, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.595667],\n            \"uv_lower_left\": [0.199219, 0.426758],\n            \"uv_upper_right\": [0.253906, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"85\": {\n            \"ascii_index\": 85,\n            \"xy_lower_left\": [0.025300, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.298828, 0.425781],\n            \"uv_upper_right\": [0.346680, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"86\": {\n            \"ascii_index\": 86,\n            \"xy_lower_left\": [-0.006958, -0.039216],\n            \"xy_upper_right\": [0.466635, 0.595667],\n            \"uv_lower_left\": [0.398438, 0.426758],\n            \"uv_upper_right\": [0.454102, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"87\": {\n            \"ascii_index\": 87,\n            \"xy_lower_left\": [-0.031151, -0.039216],\n            \"xy_upper_right\": [0.482764, 0.595667],\n            \"uv_lower_left\": [0.498047, 0.426758],\n            \"uv_upper_right\": [0.558594, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"88\": {\n            \"ascii_index\": 88,\n            \"xy_lower_left\": [-0.015022, -0.039216],\n            \"xy_upper_right\": [0.466635, 0.595667],\n            \"uv_lower_left\": [0.597656, 0.426758],\n            \"uv_upper_right\": [0.654297, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"89\": {\n            \"ascii_index\": 89,\n            \"xy_lower_left\": [-0.015022, -0.039216],\n            \"xy_upper_right\": [0.474700, 0.595667],\n            \"uv_lower_left\": [0.697266, 0.426758],\n            \"uv_upper_right\": [0.754883, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"90\": {\n            \"ascii_index\": 90,\n            \"xy_lower_left\": [0.017236, -0.039216],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.796875, 0.426758],\n            \"uv_upper_right\": [0.845703, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"91\": {\n            \"ascii_index\": 91,\n            \"xy_lower_left\": [0.105946, -0.119861],\n            \"xy_upper_right\": [0.385990, 0.676312],\n            \"uv_lower_left\": [0.896484, 0.407227],\n            \"uv_upper_right\": [0.928711, 0.501953],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"92\": {\n            \"ascii_index\": 92,\n            \"xy_lower_left\": [0.009171, -0.119861],\n            \"xy_upper_right\": [0.442441, 0.676312],\n            \"uv_lower_left\": [0.000000, 0.307617],\n            \"uv_upper_right\": [0.050781, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"93\": {\n            \"ascii_index\": 93,\n            \"xy_lower_left\": [0.065623, -0.119861],\n            \"xy_upper_right\": [0.345667, 0.676312],\n            \"uv_lower_left\": [0.099609, 0.307617],\n            \"uv_upper_right\": [0.131836, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"94\": {\n            \"ascii_index\": 94,\n            \"xy_lower_left\": [0.017236, 0.218849],\n            \"xy_upper_right\": [0.442441, 0.595667],\n            \"uv_lower_left\": [0.199219, 0.358398],\n            \"uv_upper_right\": [0.249023, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"95\": {\n            \"ascii_index\": 95,\n            \"xy_lower_left\": [0.001107, -0.119861],\n            \"xy_upper_right\": [0.450506, 0.031151],\n            \"uv_lower_left\": [0.298828, 0.385742],\n            \"uv_upper_right\": [0.351562, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"96\": {\n            \"ascii_index\": 96,\n            \"xy_lower_left\": [0.073688, 0.452720],\n            \"xy_upper_right\": [0.329538, 0.644054],\n            \"uv_lower_left\": [0.398438, 0.380859],\n            \"uv_upper_right\": [0.427734, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"97\": {\n            \"ascii_index\": 97,\n            \"xy_lower_left\": [0.009171, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.466635],\n            \"uv_lower_left\": [0.498047, 0.341797],\n            \"uv_upper_right\": [0.547852, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"98\": {\n            \"ascii_index\": 98,\n            \"xy_lower_left\": [0.025300, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.597656, 0.326172],\n            \"uv_upper_right\": [0.645508, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"99\": {\n            \"ascii_index\": 99,\n            \"xy_lower_left\": [0.017236, -0.047280],\n            \"xy_upper_right\": [0.442441, 0.466635],\n            \"uv_lower_left\": [0.697266, 0.341797],\n            \"uv_upper_right\": [0.747070, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"100\": {\n            \"ascii_index\": 100,\n            \"xy_lower_left\": [0.017236, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.796875, 0.326172],\n            \"uv_upper_right\": [0.845703, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"101\": {\n            \"ascii_index\": 101,\n            \"xy_lower_left\": [0.017236, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.466635],\n            \"uv_lower_left\": [0.896484, 0.341797],\n            \"uv_upper_right\": [0.945312, 0.402344],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"102\": {\n            \"ascii_index\": 102,\n            \"xy_lower_left\": [-0.006958, -0.039216],\n            \"xy_upper_right\": [0.450506, 0.595667],\n            \"uv_lower_left\": [0.000000, 0.227539],\n            \"uv_upper_right\": [0.053711, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"103\": {\n            \"ascii_index\": 103,\n            \"xy_lower_left\": [0.017236, -0.176312],\n            \"xy_upper_right\": [0.434377, 0.466635],\n            \"uv_lower_left\": [0.099609, 0.226562],\n            \"uv_upper_right\": [0.148438, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"104\": {\n            \"ascii_index\": 104,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.434377, 0.595667],\n            \"uv_lower_left\": [0.199219, 0.227539],\n            \"uv_upper_right\": [0.247070, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"105\": {\n            \"ascii_index\": 105,\n            \"xy_lower_left\": [0.017236, -0.039216],\n            \"xy_upper_right\": [0.466635, 0.627925],\n            \"uv_lower_left\": [0.298828, 0.223633],\n            \"uv_upper_right\": [0.351562, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"106\": {\n            \"ascii_index\": 106,\n            \"xy_lower_left\": [0.017236, -0.176312],\n            \"xy_upper_right\": [0.385990, 0.627925],\n            \"uv_lower_left\": [0.398438, 0.207031],\n            \"uv_upper_right\": [0.441406, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"107\": {\n            \"ascii_index\": 107,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.466635, 0.595667],\n            \"uv_lower_left\": [0.498047, 0.227539],\n            \"uv_upper_right\": [0.549805, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"108\": {\n            \"ascii_index\": 108,\n            \"xy_lower_left\": [-0.023087, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.595667],\n            \"uv_lower_left\": [0.597656, 0.227539],\n            \"uv_upper_right\": [0.654297, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"109\": {\n            \"ascii_index\": 109,\n            \"xy_lower_left\": [0.001107, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.466635],\n            \"uv_lower_left\": [0.697266, 0.243164],\n            \"uv_upper_right\": [0.750977, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"110\": {\n            \"ascii_index\": 110,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.434377, 0.466635],\n            \"uv_lower_left\": [0.796875, 0.243164],\n            \"uv_upper_right\": [0.844727, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"111\": {\n            \"ascii_index\": 111,\n            \"xy_lower_left\": [0.017236, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.466635],\n            \"uv_lower_left\": [0.896484, 0.242188],\n            \"uv_upper_right\": [0.945312, 0.302734],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"112\": {\n            \"ascii_index\": 112,\n            \"xy_lower_left\": [0.025300, -0.176312],\n            \"xy_upper_right\": [0.434377, 0.466635],\n            \"uv_lower_left\": [0.000000, 0.126953],\n            \"uv_upper_right\": [0.047852, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"113\": {\n            \"ascii_index\": 113,\n            \"xy_lower_left\": [0.017236, -0.176312],\n            \"xy_upper_right\": [0.434377, 0.466635],\n            \"uv_lower_left\": [0.099609, 0.126953],\n            \"uv_upper_right\": [0.148438, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"114\": {\n            \"ascii_index\": 114,\n            \"xy_lower_left\": [0.041429, -0.039216],\n            \"xy_upper_right\": [0.450506, 0.466635],\n            \"uv_lower_left\": [0.199219, 0.143555],\n            \"uv_upper_right\": [0.247070, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"115\": {\n            \"ascii_index\": 115,\n            \"xy_lower_left\": [0.017236, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.466635],\n            \"uv_lower_left\": [0.298828, 0.142578],\n            \"uv_upper_right\": [0.347656, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"116\": {\n            \"ascii_index\": 116,\n            \"xy_lower_left\": [-0.006958, -0.039216],\n            \"xy_upper_right\": [0.442441, 0.579538],\n            \"uv_lower_left\": [0.398438, 0.129883],\n            \"uv_upper_right\": [0.451172, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"117\": {\n            \"ascii_index\": 117,\n            \"xy_lower_left\": [0.025300, -0.047280],\n            \"xy_upper_right\": [0.434377, 0.458571],\n            \"uv_lower_left\": [0.498047, 0.143555],\n            \"uv_upper_right\": [0.545898, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"118\": {\n            \"ascii_index\": 118,\n            \"xy_lower_left\": [-0.006958, -0.039216],\n            \"xy_upper_right\": [0.458571, 0.458571],\n            \"uv_lower_left\": [0.597656, 0.144531],\n            \"uv_upper_right\": [0.652344, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"119\": {\n            \"ascii_index\": 119,\n            \"xy_lower_left\": [-0.023087, -0.039216],\n            \"xy_upper_right\": [0.474700, 0.458571],\n            \"uv_lower_left\": [0.697266, 0.144531],\n            \"uv_upper_right\": [0.755859, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"120\": {\n            \"ascii_index\": 120,\n            \"xy_lower_left\": [-0.006958, -0.039216],\n            \"xy_upper_right\": [0.466635, 0.458571],\n            \"uv_lower_left\": [0.796875, 0.144531],\n            \"uv_upper_right\": [0.852539, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"121\": {\n            \"ascii_index\": 121,\n            \"xy_lower_left\": [-0.006958, -0.176312],\n            \"xy_upper_right\": [0.458571, 0.458571],\n            \"uv_lower_left\": [0.896484, 0.127930],\n            \"uv_upper_right\": [0.951172, 0.203125],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"122\": {\n            \"ascii_index\": 122,\n            \"xy_lower_left\": [0.025300, -0.039216],\n            \"xy_upper_right\": [0.434377, 0.458571],\n            \"uv_lower_left\": [0.000000, 0.044922],\n            \"uv_upper_right\": [0.047852, 0.103516],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"123\": {\n            \"ascii_index\": 123,\n            \"xy_lower_left\": [0.009171, -0.119861],\n            \"xy_upper_right\": [0.426312, 0.676312],\n            \"uv_lower_left\": [0.099609, 0.008789],\n            \"uv_upper_right\": [0.148438, 0.103516],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"124\": {\n            \"ascii_index\": 124,\n            \"xy_lower_left\": [0.146268, -0.119861],\n            \"xy_upper_right\": [0.305345, 0.676312],\n            \"uv_lower_left\": [0.199219, 0.008789],\n            \"uv_upper_right\": [0.216797, 0.103516],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"125\": {\n            \"ascii_index\": 125,\n            \"xy_lower_left\": [0.025300, -0.119861],\n            \"xy_upper_right\": [0.442441, 0.676312],\n            \"uv_lower_left\": [0.298828, 0.008789],\n            \"uv_upper_right\": [0.347656, 0.103516],\n            \"advance\": [0.451613, 0.000000]\n        },\n        \"126\": {\n            \"ascii_index\": 126,\n            \"xy_lower_left\": [0.009171, 0.146268],\n            \"xy_upper_right\": [0.450506, 0.394054],\n            \"uv_lower_left\": [0.398438, 0.075195],\n            \"uv_upper_right\": [0.450195, 0.103516],\n            \"advance\": [0.451613, 0.000000]\n        }\n    },\n    \"kerning\": {\n    }\n}\n"
  },
  {
    "path": "meta-quest/cp_from_sdk",
    "content": "rm -rf splatapult\ncp -r ovr_openxr_mobile_sdk_59.0/XrSamples/splatapult splatapult"
  },
  {
    "path": "meta-quest/splatapult/.gitignore",
    "content": "# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Log/OS Files\n*.log\n\n# Android Studio generated files and folders\ncaptures/\n.externalNativeBuild/\n.cxx/\n*.apk\noutput.json\n\n# IntelliJ\n*.iml\n.idea/\nmisc.xml\ndeploymentTargetDropDown.xml\nrender.experimental.xml\n\n# Keystore files\n*.jks\n*.keystore\n\n# Google Services (e.g. APIs or Firebase)\ngoogle-services.json\n\n# Android Profiling\n*.hprof"
  },
  {
    "path": "meta-quest/splatapult/Projects/Android/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"io.github.hyperlogic.splatapult\"\n    android:versionCode=\"1\"\n    android:versionName=\"1.0\"\n    android:installLocation=\"auto\"\n    >\n  <!-- Tell the system this app requires OpenGL ES 3.1. -->\n  <uses-feature\n      android:glEsVersion=\"0x00030001\"\n      android:required=\"true\"\n      />\n\n  <uses-feature\n      android:name=\"android.hardware.vr.headtracking\"\n      android:required=\"true\"\n      />\n  <!-- Volume Control -->\n  <uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />\n  <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n\n  <!-- Enable report events to Telemetry Service -->\n  <uses-permission android:name=\"com.oculus.permission.REPORT_EVENTS\" />\n  <uses-permission android:name=\"com.oculus.permission.REPORT_EVENTS_DEBUG\" />\n\n  <application\n      android:allowBackup=\"false\"\n      android:label=\"splatapult\"\n      >\n    <meta-data\n        android:name=\"com.samsung.android.vr.application.mode\"\n        android:value=\"vr_only\"\n        />\n    <meta-data android:name=\"com.oculus.supportedDevices\" android:value=\"all\" />\n    <!-- launchMode is set to singleTask because there should never be multiple copies of the app running -->\n    <!-- Theme.Black.NoTitleBar.Fullscreen gives solid black instead of a (bad stereoscopic) gradient on app transition -->\n    <!-- If targeting API level 24+, configChanges should additionally include 'density'. -->\n    <!-- If targeting API level 24+, android:resizeableActivity=\"false\" should be added. -->\n    <activity\n        android:name=\"com.oculus.NativeActivity\"\n        android:theme=\"@android:style/Theme.Black.NoTitleBar.Fullscreen\"\n        android:launchMode=\"singleTask\"\n        android:screenOrientation=\"landscape\"\n        android:excludeFromRecents=\"false\"\n        android:configChanges=\"screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode\"\n        >\n      <!-- Tell NativeActivity the name of the .so -->\n      <meta-data android:name=\"android.app.lib_name\" android:value=\"splatapult\" />\n      <!-- This filter lets the apk show up as a launchable icon. -->\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\" />\n        <category android:name=\"com.oculus.intent.category.VR\" />\n        <category android:name=\"android.intent.category.LAUNCHER\" />\n      </intent-filter>\n    </activity>\n  </application>\n</manifest>\n"
  },
  {
    "path": "meta-quest/splatapult/Projects/Android/build.bat",
    "content": "@rem Only edit the master copy of this file in SDK_ROOT/bin/scripts/build/perproject\n\n@setlocal enableextensions enabledelayedexpansion\n\n@if not exist \"build.gradle\" (\n    @echo Build script must be executed from project directory. & goto :Abort\n)\n\n@set P=..\n\n:TryAgain\n\n@rem @echo P = %P%\n\n@if exist \"%P%\\bin\\scripts\\build\\build.py.bat\" goto :Found\n\n@if exist \"%P%\\bin\\scripts\\build\" @echo \"Could not find build.py.bat\" & goto :Abort\n\n@set P=%P%\\..\n\n@goto :TryAgain\n\n:Found\n\n@set P=%P%\\bin\\scripts\\build\n@call %P%\\build.py.bat %1 %2 %3 %4 %5\n@goto :End\n\n:Abort\n\n:End\n"
  },
  {
    "path": "meta-quest/splatapult/Projects/Android/build.gradle",
    "content": "\napply plugin: 'com.android.application'\napply from: \"${rootProject.projectDir}/VrApp.gradle\"\n\ntask copyFiles(type: Copy) {\n  copy {\n    from '../../../../../../shader'\n    into '../../assets/shader'\n  }\n  copy {\n    from '../../../../../../font'\n    into '../../assets/font'\n  }\n  copy {\n    from '../../../../../../texture'\n    into '../../assets/texture'\n  }\n  copy {\n    from '../../../../../../data/sh_test'\n    into '../../assets/data/sh_test'\n  }\n  copy {\n    from '../../../../../../data/livingroom'\n    into '../../assets/data/livingroom'\n  }\n}\n\ngradle.projectsEvaluated {\n  preBuild.dependsOn(copyFiles)\n}\n\nandroid {\n  // This is the name of the generated apk file, which will have\n  // -debug.apk or -release.apk appended to it.\n  // The filename doesn't effect the Android installation process.\n  // Use only letters to remain compatible with the package name.\n  project.archivesBaseName = \"splatapult\"\n\n\n  defaultConfig {\n    // Gradle replaces the manifest package with this value, which must\n    // be unique on a system.  If you don't change it, a new app\n    // will replace an older one.\n    applicationId \"com.oculus.sdk.\" + project.archivesBaseName\n    minSdkVersion 24\n    targetSdkVersion 25\n    compileSdkVersion 26\n\n    // override app plugin abiFilters for 64-bit support\n    externalNativeBuild {\n        ndk {\n                abiFilters 'arm64-v8a'\n        }\n        ndkBuild {\n                abiFilters 'arm64-v8a'\n        }\n    }\n  }\n\n  sourceSets {\n    main {\n      manifest.srcFile 'AndroidManifest.xml'\n      java.srcDirs = ['../../java']\n      jniLibs.srcDir 'libs'\n\n      assets.srcDirs = ['../../assets']\n    }\n  }\n\n  lintOptions {\n    disable 'ExpiredTargetSdkVersion'\n  }\n}\n"
  },
  {
    "path": "meta-quest/splatapult/Projects/Android/build.py",
    "content": "#!/usr/bin/python\n# (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.\n\n# This first bit of code is common bootstrapping code\n# to determine the SDK root, and to set up the import\n# path for additional python code.\n\n# begin bootstrap\nimport os\nimport sys\n\n\ndef init():\n    root = os.path.realpath(os.path.dirname(os.path.realpath(__file__)))\n    os.chdir(root)  # make sure we are always executing from the project directory\n    while os.path.isdir(os.path.join(root, \"bin/scripts/build\")) == False:\n        root = os.path.realpath(os.path.join(root, \"..\"))\n        if (\n            len(root) <= 5\n        ):  # Should catch both Posix and Windows root directories (e.g. '/' and 'C:\\')\n            print(\"Unable to find SDK root. Exiting.\")\n            sys.exit(1)\n    root = os.path.abspath(root)\n    os.environ[\"OCULUS_SDK_PATH\"] = root\n    sys.path.append(root + \"/bin/scripts/build\")\n\n\ninit()\nimport ovrbuild\n\novrbuild.init()\n# end bootstrap\n\n\novrbuild.build()\n"
  },
  {
    "path": "meta-quest/splatapult/Projects/Android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4g\n"
  },
  {
    "path": "meta-quest/splatapult/Projects/Android/jni/Android.mk",
    "content": "LOCAL_PATH := $(call my-dir)\ninclude $(CLEAR_VARS)\n\nLOCAL_MODULE := splatapult\n\nLOCAL_CFLAGS += -Werror\n\n# need execptions for json and radix sort.\nLOCAL_CFLAGS += -fexceptions\n\n# This should be set via an environment var\n# ANDROID_VCPKG_DIR := C:/msys64/home/hyperlogic/code/vcpkg/installed/arm64-android\n\nLOCAL_C_INCLUDES := \\\n                    $(LOCAL_PATH)/../../../../../../../src/ \\\n\t\t\t\t\t$(LOCAL_PATH)/../../../../../1stParty/OVR/Include \\\n\t\t\t\t\t$(LOCAL_PATH)/../../../../../OpenXr/Include \\\n\t\t\t\t\t$(LOCAL_PATH)/../../../../../3rdParty/khronos/openxr/OpenXR-SDK/include/ \\\n\t\t\t\t\t$(LOCAL_PATH)/../../../../../3rdParty/khronos/openxr/OpenXR-SDK/src/common/ \\\n\t\t\t\t\t$(ANDROID_VCPKG_DIR)/include \\\n\nLOCAL_SRC_PATH := ../../../../../../../src\nLOCAL_SRC_FILES\t:=  $(LOCAL_SRC_PATH)/core/debugrenderer.cpp \\\n\t\t\t\t    $(LOCAL_SRC_PATH)/core/image.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/core/log.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/core/program.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/core/texture.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/core/util.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/core/vertexbuffer.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/core/textrenderer.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/core/xrbuddy.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/app.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/android_main.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/camerasconfig.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/flycam.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/gaussiancloud.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/magiccarpet.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/ply.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/pointcloud.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/pointrenderer.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/splatrenderer.cpp \\\n\t\t\t\t\t$(LOCAL_SRC_PATH)/vrconfig.cpp \\\n\nLOCAL_LDLIBS := -lEGL -lGLESv3 -landroid -llog -lpng -lpng16 -lz\n\nLOCAL_LDFLAGS := -u ANativeActivity_onCreate\nLOCAL_LDLIBS += -L$(ANDROID_VCPKG_DIR)/lib\n\nLOCAL_STATIC_LIBRARIES := android_native_app_glue\nLOCAL_SHARED_LIBRARIES := openxr_loader\n\ninclude $(BUILD_SHARED_LIBRARY)\n\n$(call import-module,OpenXR/Projects/AndroidPrebuilt/jni)\n$(call import-module,android/native_app_glue)\n"
  },
  {
    "path": "meta-quest/splatapult/Projects/Android/jni/Application.mk",
    "content": "# MAKEFILE_LIST specifies the current used Makefiles, of which this is the last\n# one. I use that to obtain the Application.mk dir then import the root\n# Application.mk.\nROOT_DIR := $(dir $(lastword $(MAKEFILE_LIST)))../../../../../\n\nNDK_MODULE_PATH := $(ROOT_DIR)\n\n# ndk-r14 introduced failure for missing dependencies. If 'false', the clean\n# step will error as we currently remove prebuilt artifacts on clean.\nAPP_ALLOW_MISSING_DEPS=true\n"
  },
  {
    "path": "meta-quest/splatapult/Projects/Android/settings.gradle",
    "content": "rootProject.projectDir = new File(settingsDir, '../../../..')\nrootProject.name = \"splatapult\"\n\ninclude ':', \\\n    ':XrSamples:splatapult:Projects:Android'\n"
  },
  {
    "path": "meta-quest/splatapult/assets/.gitignore",
    "content": "font/\nshader/\ntexture/\ndata/"
  },
  {
    "path": "meta-quest/splatapult/assets/donotedelete.txt",
    "content": ""
  },
  {
    "path": "meta-quest/splatapult/java/com/oculus/NativeActivity.java",
    "content": "// Copyright (c) Facebook Technologies, LLC and its affiliates. All Rights reserved.\npackage com.oculus;\n\nimport android.content.pm.PackageManager;\nimport android.os.Bundle;\nimport android.util.Log;\n\n/**\n * When using NativeActivity, we currently need to handle loading of dependent shared libraries\n * manually before a shared library that depends on them is loaded, since there is not currently a\n * way to specify a shared library dependency for NativeActivity via the manifest meta-data.\n *\n * <p>The simplest method for doing so is to subclass NativeActivity with an empty activity that\n * calls System.loadLibrary on the dependent libraries, which is unfortunate when the goal is to\n * write a pure native C/C++ only Android activity.\n *\n * <p>A native-code only solution is to load the dependent libraries dynamically using dlopen().\n * However, there are a few considerations, see:\n * https://groups.google.com/forum/#!msg/android-ndk/l2E2qh17Q6I/wj6s_6HSjaYJ\n *\n * <p>1. Only call dlopen() if you're sure it will succeed as the bionic dynamic linker will\n * remember if dlopen failed and will not re-try a dlopen on the same lib a second time.\n *\n * <p>2. Must remember what libraries have already been loaded to avoid infinitely looping when\n * libraries have circular dependencies.\n */\npublic class NativeActivity extends android.app.NativeActivity {\n  private static final String PERMISSION_USE_SCENE = \"com.oculus.permission.USE_SCENE\";\n  private static final int REQUEST_CODE_PERMISSION_USE_SCENE = 1;\n  private static final String TAG = \"splatapult\";\n\n  static {\n    System.loadLibrary(\"openxr_loader\");\n    System.loadLibrary(\"splatapult\");\n  }\n\n  @Override\n  protected void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    requestScenePermissionIfNeeded();\n  }\n\n  private void requestScenePermissionIfNeeded() {\n    Log.d(TAG, \"requestScenePermissionIfNeeded\");\n    /*\n    if (checkSelfPermission(PERMISSION_USE_SCENE) != PackageManager.PERMISSION_GRANTED) {\n      Log.d(TAG, \"Permission has not been granted, request \" + PERMISSION_USE_SCENE);\n      requestPermissions(\n          new String[] {PERMISSION_USE_SCENE}, REQUEST_CODE_PERMISSION_USE_SCENE);\n    }\n    */\n  }\n}\n"
  },
  {
    "path": "shader/carpet_frag.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// fullbright textured mesh\n//\n\n/*%%HEADER%%*/\n\nuniform sampler2D colorTex;\n\nin vec2 frag_uv;\n\nout vec4 out_color;\n\nvoid main()\n{\n    vec4 texColor = texture(colorTex, frag_uv);\n\n    // premultiplied alpha blending\n    out_color.rgb = texColor.a * texColor.rgb;\n    out_color.a = texColor.a;\n}\n"
  },
  {
    "path": "shader/carpet_vert.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// fullbright textured mesh\n//\n\n/*%%HEADER%%*/\n\nuniform mat4 modelViewProjMat;\n\nin vec3 position;\nin vec2 uv;\n\nout vec2 frag_uv;\n\nvoid main(void)\n{\n    gl_Position = modelViewProjMat * vec4(position, 1);\n    frag_uv = uv;\n}\n"
  },
  {
    "path": "shader/debugdraw_frag.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// No lighting at all, solid color\n//\n\n/*%%HEADER%%*/\n\nin vec4 frag_color;\nout vec4 out_color;\n\nvoid main()\n{\n    // pre-multiplied alpha blending\n    out_color.rgb = frag_color.a * frag_color.rgb;\n    out_color.a = frag_color.a;\n}\n"
  },
  {
    "path": "shader/debugdraw_vert.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// No lighting at all, solid color\n//\n\n/*%%HEADER%%*/\n\nuniform mat4 modelViewProjMat;\n\nin vec3 position;\nin vec4 color;\n\nout vec4 frag_color;\n\nvoid main(void)\n{\n    gl_Position = modelViewProjMat * vec4(position, 1);\n\tfrag_color = color;\n}\n"
  },
  {
    "path": "shader/desktop_frag.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n/*%%HEADER%%*/\n\n/*%%DEFINES%%*/\n\nuniform vec4 color;\nuniform sampler2D colorTexture;\n\nin vec2 frag_uv;\n\nout vec4 out_color;\n\nvoid main(void)\n{\n#ifdef USE_SUPERSAMPLING\n    // per pixel screen space partial derivatives\n    vec2 dx = dFdx(frag_uv) * 0.25; // horizontal offset\n    vec2 dy = dFdy(frag_uv) * 0.25; // vertical offset\n\n    // supersampled 2x2 ordered grid\n    vec4 texColor = vec4(0);\n    texColor += texture(colorTexture, vec2(frag_uv + dx + dy));\n    texColor += texture(colorTexture, vec2(frag_uv - dx + dy));\n    texColor += texture(colorTexture, vec2(frag_uv + dx - dy));\n    texColor += texture(colorTexture, vec2(frag_uv - dx - dy));\n    texColor *= 0.25;\n#else\n    vec4 texColor = texture(colorTexture, frag_uv);\n#endif\n\n    // premultiplied alpha blending\n    out_color.rgb = color.a * color.rgb * texColor.rgb;\n    out_color.a = color.a * texColor.a;\n}\n"
  },
  {
    "path": "shader/desktop_vert.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// fullbright textured mesh\n//\n\n/*%%HEADER%%*/\n\nuniform mat4 modelViewProjMat;\n\nin vec3 position;\nin vec2 uv;\n\nout vec2 frag_uv;\n\nvoid main(void)\n{\n    gl_Position = modelViewProjMat * vec4(position, 1);\n    frag_uv = uv;\n}\n"
  },
  {
    "path": "shader/multi_radixsort.glsl",
    "content": "/**\n* VkRadixSort written by Mirco Werner: https://github.com/MircoWerner/VkRadixSort\n* Based on implementation of Intel's Embree: https://github.com/embree/embree/blob/v4.0.0-ploc/kernels/rthwif/builder/gpu/sort.h\n*/\n#version 460\n//#extension GL_GOOGLE_include_directive: enable\n#extension GL_KHR_shader_subgroup_basic: enable\n#extension GL_KHR_shader_subgroup_arithmetic: enable\n#extension GL_KHR_shader_subgroup_ballot: enable\n\n#define WORKGROUP_SIZE 256// assert WORKGROUP_SIZE >= RADIX_SORT_BINS\n#define RADIX_SORT_BINS 256\n#define SUBGROUP_SIZE 32// 32 NVIDIA; 64 AMD\n\n#define BITS 32// sorting uint32_t\n\nlayout (local_size_x = WORKGROUP_SIZE) in;\n\nuniform uint g_num_elements;\nuniform uint g_shift;\nuniform uint g_num_workgroups;\nuniform uint g_num_blocks_per_workgroup;\n\nlayout (std430, binding = 0) buffer elements_in {\n    uint g_elements_in[];\n};\n\nlayout (std430, binding = 1) buffer elements_out {\n    uint g_elements_out[];\n};\n\nlayout (std430, binding = 2) buffer indices_in {\n    uint g_indices_in[];\n};\n\nlayout (std430, binding = 3) buffer indices_out {\n    uint g_indices_out[];\n};\n\nlayout (std430, binding = 4) buffer histograms {\n// [histogram_of_workgroup_0 | histogram_of_workgroup_1 | ... ]\n    uint g_histograms[];// |g_histograms| = RADIX_SORT_BINS * #WORKGROUPS = RADIX_SORT_BINS * g_num_workgroups\n};\n\nshared uint[RADIX_SORT_BINS / SUBGROUP_SIZE] sums;// subgroup reductions\nshared uint[RADIX_SORT_BINS] global_offsets;// global exclusive scan (prefix sum)\n\nstruct BinFlags {\n    uint flags[WORKGROUP_SIZE / BITS];\n};\nshared BinFlags[RADIX_SORT_BINS] bin_flags;\n\nvoid main() {\n    uint gID = gl_GlobalInvocationID.x;\n    uint lID = gl_LocalInvocationID.x;\n    uint wID = gl_WorkGroupID.x;\n    uint sID = gl_SubgroupID;\n    uint lsID = gl_SubgroupInvocationID;\n\n    uint local_histogram = 0;\n    uint prefix_sum = 0;\n    uint histogram_count = 0;\n\n    if (lID < RADIX_SORT_BINS) {\n        uint count = 0;\n        for (uint j = 0; j < g_num_workgroups; j++) {\n            const uint t = g_histograms[RADIX_SORT_BINS * j + lID];\n            local_histogram = (j == wID) ? count : local_histogram;\n            count += t;\n        }\n        histogram_count = count;\n        const uint sum = subgroupAdd(histogram_count);\n        prefix_sum = subgroupExclusiveAdd(histogram_count);\n        if (subgroupElect()) {\n            // one thread inside the warp/subgroup enters this section\n            sums[sID] = sum;\n        }\n    }\n    barrier();\n\n    if (lID < RADIX_SORT_BINS) {\n        const uint sums_prefix_sum = subgroupBroadcast(subgroupExclusiveAdd(sums[lsID]), sID);\n        const uint global_histogram = sums_prefix_sum + prefix_sum;\n        global_offsets[lID] = global_histogram + local_histogram;\n    }\n\n    //     ==== scatter keys according to global offsets =====\n    const uint flags_bin = lID / BITS;\n    const uint flags_bit = 1 << (lID % BITS);\n\n    for (uint index = 0; index < g_num_blocks_per_workgroup; index++) {\n        uint elementId = wID * g_num_blocks_per_workgroup * WORKGROUP_SIZE + index * WORKGROUP_SIZE + lID;\n\n        // initialize bin flags\n        if (lID < RADIX_SORT_BINS) {\n            for (int i = 0; i < WORKGROUP_SIZE / BITS; i++) {\n                bin_flags[lID].flags[i] = 0U;// init all bin flags to 0\n            }\n        }\n        barrier();\n\n        uint element_in = 0;\n        uint index_in = 0;\n        uint binID = 0;\n        uint binOffset = 0;\n        if (elementId < g_num_elements) {\n            element_in = g_elements_in[elementId];\n            index_in = g_indices_in[elementId];\n            binID = (element_in >> g_shift) & uint(RADIX_SORT_BINS - 1);\n            // offset for group\n            binOffset = global_offsets[binID];\n            // add bit to flag\n            atomicAdd(bin_flags[binID].flags[flags_bin], flags_bit);\n        }\n        barrier();\n\n        if (elementId < g_num_elements) {\n            // calculate output index of element\n            uint prefix = 0;\n            uint count = 0;\n            for (uint i = 0; i < WORKGROUP_SIZE / BITS; i++) {\n                const uint bits = bin_flags[binID].flags[i];\n                const uint full_count = bitCount(bits);\n                const uint partial_count = bitCount(bits & (flags_bit - 1));\n                prefix += (i < flags_bin) ? full_count : 0U;\n                prefix += (i == flags_bin) ? partial_count : 0U;\n                count += full_count;\n            }\n            g_elements_out[binOffset + prefix] = element_in;\n            g_indices_out[binOffset + prefix] = index_in;\n            if (prefix == count - 1) {\n                atomicAdd(global_offsets[binID], count);\n            }\n        }\n\n        barrier();\n    }\n}\n"
  },
  {
    "path": "shader/multi_radixsort_histograms.glsl",
    "content": "/**\n* VkRadixSort written by Mirco Werner: https://github.com/MircoWerner/VkRadixSort\n* Based on implementation of Intel's Embree: https://github.com/embree/embree/blob/v4.0.0-ploc/kernels/rthwif/builder/gpu/sort.h\n*/\n#version 460\n//#extension GL_GOOGLE_include_directive: enable\n\n#define WORKGROUP_SIZE 256 // assert WORKGROUP_SIZE >= RADIX_SORT_BINS\n#define RADIX_SORT_BINS 256\n\nuniform uint g_num_elements;\nuniform uint g_shift;\n//uniform uint g_num_workgroups;\nuniform uint g_num_blocks_per_workgroup;\n\nlayout (local_size_x = WORKGROUP_SIZE) in;\n\nlayout (std430, binding = 0) buffer elements_in {\n    uint g_elements_in[];\n};\n\nlayout (std430, binding = 1) buffer histograms {\n    // [histogram_of_workgroup_0 | histogram_of_workgroup_1 | ... ]\n    uint g_histograms[]; // |g_histograms| = RADIX_SORT_BINS * #WORKGROUPS\n};\n\nshared uint[RADIX_SORT_BINS] histogram;\n\nvoid main() {\n    uint gID = gl_GlobalInvocationID.x;\n    uint lID = gl_LocalInvocationID.x;\n    uint wID = gl_WorkGroupID.x;\n\n    // initialize histogram\n    if (lID < RADIX_SORT_BINS) {\n        histogram[lID] = 0U;\n    }\n    barrier();\n\n    for (uint index = 0; index < g_num_blocks_per_workgroup; index++) {\n        uint elementId = wID * g_num_blocks_per_workgroup * WORKGROUP_SIZE + index * WORKGROUP_SIZE + lID;\n        if (elementId < g_num_elements) {\n            // determine the bin\n            const uint bin = (g_elements_in[elementId] >> g_shift) & (RADIX_SORT_BINS - 1);\n            // increment the histogram\n            atomicAdd(histogram[bin], 1U);\n        }\n    }\n    barrier();\n\n    if (lID < RADIX_SORT_BINS) {\n        g_histograms[RADIX_SORT_BINS * wID + lID] = histogram[lID];\n    }\n}\n"
  },
  {
    "path": "shader/point_frag.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// fullbright textured particle\n//\n\n/*%%HEADER%%*/\n\nuniform sampler2D colorTex;\n\nin vec2 frag_uv;\nin vec4 frag_color;\n\nout vec4 out_color;\n\nvoid main()\n{\n    vec4 texColor = texture(colorTex, frag_uv);\n\n    // premultiplied alpha blending\n    out_color.rgb = frag_color.a * frag_color.rgb * texColor.rgb;\n    out_color.a = frag_color.a * texColor.a;\n}\n"
  },
  {
    "path": "shader/point_geom.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n/*%%HEADER%%*/\n\nuniform float pointSize;\nuniform float invAspectRatio;\n\nlayout(points) in;\nlayout(triangle_strip, max_vertices = 4) out;\n\nin vec4 geom_color[];\n\nout vec4 frag_color;\nout vec2 frag_uv;\n\nvoid main()\n{\n\tvec2 offset = vec2(pointSize * invAspectRatio, pointSize);\n\n\t// bottom-left vertex\n    frag_uv = vec2(0.0, 0.0);\n    frag_color = geom_color[0];\n    gl_Position = gl_in[0].gl_Position + vec4(-offset.x, -offset.y, 0.0, 0.0);\n    EmitVertex();\n\n    // bottom-right vertex\n    frag_uv = vec2(1.0, 0.0);\n    frag_color = geom_color[0];\n    gl_Position = gl_in[0].gl_Position + vec4(offset.x, -offset.y, 0.0, 0.0);\n    EmitVertex();\n\n    // top-left vertex\n    frag_uv = vec2(0.0, 1.0);\n    frag_color = geom_color[0];\n    gl_Position = gl_in[0].gl_Position + vec4(-offset.x, offset.y, 0.0, 0.0);\n    EmitVertex();\n\n    // top-right vertex\n    frag_uv = vec2(1.0, 1.0);\n    frag_color = geom_color[0];\n    gl_Position = gl_in[0].gl_Position + vec4(offset.x, offset.y, 0.0, 0.0);\n    EmitVertex();\n\n    EndPrimitive();\n}\n"
  },
  {
    "path": "shader/point_vert.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// fullbright textured particle\n//\n\n/*%%HEADER%%*/\n\nuniform float pointSize;\nuniform float invAspectRatio;\nuniform mat4 modelViewMat;\nuniform mat4 projMat;\n\nin vec4 position;\nin vec4 color;\n\nout vec4 geom_color;\n\nvoid main(void)\n{\n    gl_Position = projMat * modelViewMat * position;\n    geom_color = color;\n}\n"
  },
  {
    "path": "shader/presort_compute.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n/*%%HEADER%%*/\n\nlayout(local_size_x = 256) in;\n\nuniform mat4 modelViewProj;\nuniform vec2 nearFar;\nuniform uint keyMax;\n\nlayout(binding = 4, offset = 0) uniform atomic_uint output_count;\n\nlayout(std430, binding = 0) readonly buffer PosBuffer\n{\n    vec4 positions[];\n};\n\nlayout(std430, binding = 1) writeonly buffer OutputBuffer\n{\n    uint quantizedZs[];\n};\n\nlayout(std430, binding = 2) writeonly buffer OutputBuffer2\n{\n    uint indices[];\n};\n\nvoid main()\n{\n    uint idx = gl_GlobalInvocationID.x;\n\n\tuint len = uint(positions.length());\n    if (idx >= len)\n    {\n        return;\n    }\n\n    // NOTE: alpha is encoded into the w component of the positions\n    vec4 p = modelViewProj * vec4(positions[idx].xyz, 1.0f);\n    float depth = p.w;\n    float xx = p.x / depth;\n    float yy = p.y / depth;\n\n    const float CLIP = 1.5f;\n    if (depth > 0.0f && xx < CLIP && xx > -CLIP && yy < CLIP && yy > -CLIP)\n    {\n        uint count = atomicCounterIncrement(output_count);\n        // 16.16 fixed point\n        //uint fixedPointZ = uint(0xffffffff) - uint(clamp(depth, 0.0f, 65535.0f) * 65536.0f);\n\t\tuint fixedPointZ = keyMax - uint((depth / nearFar.y) * keyMax);\n        quantizedZs[count] = fixedPointZ;\n        indices[count] = idx;\n    }\n}\n"
  },
  {
    "path": "shader/single_radixsort.glsl",
    "content": "/**\n* VkRadixSort written by Mirco Werner: https://github.com/MircoWerner/VkRadixSort\n* Based on implementation of Intel's Embree: https://github.com/embree/embree/blob/v4.0.0-ploc/kernels/rthwif/builder/gpu/sort.h\n*/\n#version 460\n#extension GL_KHR_shader_subgroup_basic: enable\n#extension GL_KHR_shader_subgroup_arithmetic: enable\n\n#define WORKGROUP_SIZE 256// assert WORKGROUP_SIZE >= RADIX_SORT_BINS\n#define RADIX_SORT_BINS 256\n#define SUBGROUP_SIZE 32// 32 NVIDIA; 64 AMD\n\n#define BITS 32// sorting uint32_t\n#define ITERATIONS 4// 4 iterations, sorting 8 bits per iteration\n\nlayout (local_size_x = WORKGROUP_SIZE) in;\n\nuniform uint g_num_elements;\n\nlayout (std430, set = 0, binding = 0) buffer elements_in {\n    uint g_elements_in[];\n};\n\nlayout (std430, set = 0, binding = 1) buffer elements_out {\n    uint g_elements_out[];\n};\n\nlayout (std430, set = 0, binding = 2) buffer indices_in {\n    uint g_indices_in[];\n};\n\nlayout (std430, set = 0, binding = 3) buffer indices_out {\n    uint g_indices_out[];\n};\n\nshared uint[RADIX_SORT_BINS] histogram;\nshared uint[RADIX_SORT_BINS / SUBGROUP_SIZE] sums;// subgroup reductions\nshared uint[RADIX_SORT_BINS] local_offsets;// local exclusive scan (prefix sum) (inside subgroups)\nshared uint[RADIX_SORT_BINS] global_offsets;// global exclusive scan (prefix sum)\n\nstruct BinFlags {\n    uint flags[WORKGROUP_SIZE / BITS];\n};\nshared BinFlags[RADIX_SORT_BINS] bin_flags;\n\n#define ELEMENT_IN(index, iteration) (iteration % 2 == 0 ? g_elements_in[index] : g_elements_out[index])\n#define INDEX_IN(index, iteration) (iteration % 2 == 0 ? g_indices_in[index] : g_indices_out[index])\n\nvoid main() {\n    uint lID = gl_LocalInvocationID.x;\n    uint sID = gl_SubgroupID;\n    uint lsID = gl_SubgroupInvocationID;\n\n    for (uint iteration = 0; iteration < ITERATIONS; iteration++) {\n        uint shift = 8 * iteration;\n\n        // initialize histogram\n        if (lID < RADIX_SORT_BINS) {\n            histogram[lID] = 0U;\n        }\n        barrier();\n\n        for (uint ID = lID; ID < g_num_elements; ID += WORKGROUP_SIZE) {\n            // determine the bin\n            const uint bin = (ELEMENT_IN(ID, iteration) >> shift) & (RADIX_SORT_BINS - 1);\n            // increment the histogram\n            atomicAdd(histogram[bin], 1U);\n        }\n        barrier();\n\n        // subgroup reductions and subgroup prefix sums\n        if (lID < RADIX_SORT_BINS) {\n            uint histogram_count = histogram[lID];\n            uint sum = subgroupAdd(histogram_count);\n            uint prefix_sum = subgroupExclusiveAdd(histogram_count);\n            local_offsets[lID] = prefix_sum;\n            if (subgroupElect()) {\n                // one thread inside the warp/subgroup enters this section\n                sums[sID] = sum;\n            }\n        }\n        barrier();\n\n        // global prefix sums (offsets)\n        if (sID == 0) {\n            uint offset = 0;\n            for (uint i = lsID; i < RADIX_SORT_BINS; i += SUBGROUP_SIZE) {\n                global_offsets[i] = offset + local_offsets[i];\n                offset += sums[i / SUBGROUP_SIZE];\n            }\n        }\n        barrier();\n\n        //     ==== scatter keys according to global offsets =====\n        const uint flags_bin = lID / BITS;\n        const uint flags_bit = 1 << (lID % BITS);\n\n        for (uint blockID = 0; blockID < g_num_elements; blockID += WORKGROUP_SIZE) {\n            barrier();\n\n            const uint ID = blockID + lID;\n\n            // initialize bin flags\n            if (lID < RADIX_SORT_BINS) {\n                for (int i = 0; i < WORKGROUP_SIZE / BITS; i++) {\n                    bin_flags[lID].flags[i] = 0U;// init all bin flags to 0\n                }\n            }\n            barrier();\n\n            uint element_in = 0;\n            uint index_in = 0;\n            uint binID = 0;\n            uint binOffset = 0;\n            if (ID < g_num_elements) {\n                element_in = ELEMENT_IN(ID, iteration);\n                index_in = INDEX_IN(ID, iteration);\n                binID = (element_in >> shift) & uint(RADIX_SORT_BINS - 1);\n                // offset for group\n                binOffset = global_offsets[binID];\n                // add bit to flag\n                atomicAdd(bin_flags[binID].flags[flags_bin], flags_bit);\n            }\n            barrier();\n\n            if (ID < g_num_elements) {\n                // calculate output index of element\n                uint prefix = 0;\n                uint count = 0;\n                for (uint i = 0; i < WORKGROUP_SIZE / BITS; i++) {\n                    const uint bits = bin_flags[binID].flags[i];\n                    const uint full_count = bitCount(bits);\n                    const uint partial_count = bitCount(bits & (flags_bit - 1));\n                    prefix += (i < flags_bin) ? full_count : 0U;\n                    prefix += (i == flags_bin) ? partial_count : 0U;\n                    count += full_count;\n                }\n                if (iteration % 2 == 0) {\n                    g_elements_out[binOffset + prefix] = element_in;\n                    g_indices_out[binOffset + prefix] = index_in;\n                } else {\n                    g_elements_in[binOffset + prefix] = element_in;\n                    g_indices_in[binOffset + prefix] = index_in;\n                }\n                if (prefix == count - 1) {\n                    atomicAdd(global_offsets[binID], count);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "shader/splat_frag.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// 3d gaussian splat fragment shader\n//\n\n/*%%HEADER%%*/\n\nin vec4 frag_color;  // radiance of splat\nin vec4 frag_cov2inv;  // inverse of the 2D screen space covariance matrix of the guassian\nin vec2 frag_p;  // 2D screen space center of the guassian\n\nout vec4 out_color;\n\nvoid main()\n{\n    vec2 d = gl_FragCoord.xy - frag_p;\n\n    // TODO: Use texture for gaussian evaluation\n    // evaluate the gaussian\n    mat2 cov2Dinv = mat2(frag_cov2inv.xy, frag_cov2inv.zw);\n    float g = exp(-0.5f * dot(d, cov2Dinv * d));\n\n    out_color.rgb = frag_color.a * g * frag_color.rgb;\n    out_color.a = frag_color.a * g;\n\n    /*\n    // can be used to determine overdraw.\n    float epsilon = 1.0f / 256.0f;\n    out_color.rgb = frag_color.a * g * frag_color.rgb;\n    out_color.rgb = frag_color.rgb * 0.00000001f + vec3(epsilon, epsilon, epsilon);\n    out_color.a = 0.0f;\n    */\n\n    if ((frag_color.a * g) <= (1.0f / 256.0f))\n    {\n        discard;\n    }\n}\n"
  },
  {
    "path": "shader/splat_geom.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n/*%%HEADER%%*/\n\nuniform vec4 viewport;  // x, y, WIDTH, HEIGHT\n\nlayout(points) in;\nlayout(triangle_strip, max_vertices = 4) out;\n\nin vec4 geom_color[];  // radiance of splat\nin vec4 geom_cov2[];  // 2D screen space covariance matrix of the gaussian\nin vec2 geom_p[];  // the 2D screen space center of the gaussian\n\nout vec4 frag_color;  // radiance of splat\nout vec4 frag_cov2inv;  // inverse of the 2D screen space covariance matrix of the guassian\nout vec2 frag_p;  // the 2D screen space center of the gaussian\n\n// used to invert the 2D screen space covariance matrix\nmat2 inverseMat2(mat2 m)\n{\n    float det = m[0][0] * m[1][1] - m[0][1] * m[1][0];\n    mat2 inv;\n    inv[0][0] =  m[1][1] / det;\n    inv[0][1] = -m[0][1] / det;\n    inv[1][0] = -m[1][0] / det;\n    inv[1][1] =  m[0][0] / det;\n\n    return inv;\n}\n\nvoid main()\n{\n    float WIDTH = viewport.z;\n    float HEIGHT = viewport.w;\n\n    mat2 cov2D = mat2(geom_cov2[0].xy, geom_cov2[0].zw);\n\n    // we pass the inverse of the 2d covariance matrix to the pixel shader, to avoid doing a matrix inverse per pixel.\n    mat2 cov2Dinv = inverseMat2(cov2D);\n    vec4 cov2Dinv4 = vec4(cov2Dinv[0], cov2Dinv[1]); // cram it into a vec4\n\n    // discard splats that end up outside of a guard band\n    vec4 p4 = gl_in[0].gl_Position;\n    vec3 ndcP = p4.xyz / p4.w;\n    if (ndcP.z < 0.25f ||\n        ndcP.x > 2.0f || ndcP.x < -2.0f ||\n        ndcP.y > 2.0f || ndcP.y < -2.0f)\n    {\n        // discard this point\n        return;\n    }\n\n    // compute 2d extents for the splat, using covariance matrix ellipse\n    // see https://cookierobotics.com/007/\n    float k = 3.5f;\n    float a = cov2D[0][0];\n    float b = cov2D[0][1];\n    float c = cov2D[1][1];\n    float apco2 = (a + c) / 2.0f;\n    float amco2 = (a - c) / 2.0f;\n    float term = sqrt(amco2 * amco2 + b * b);\n    float maj = apco2 + term;\n    float min = apco2 - term;\n\n    float theta;\n    if (b == 0.0f)\n    {\n        theta = (a >= c) ? 0.0f : radians(90.0f);\n    }\n    else\n    {\n        theta = atan(maj - a, b);\n    }\n\n    float r1 = k * sqrt(maj);\n    float r2 = k * sqrt(min);\n    vec2 majAxis = vec2(r1 * cos(theta), r1 * sin(theta));\n    vec2 minAxis = vec2(r2 * cos(theta + radians(90.0f)), r2 * sin(theta + radians(90.0f)));\n\n    vec2 offsets[4];\n    offsets[0] = majAxis + minAxis;\n    offsets[1] = -majAxis + minAxis;\n    offsets[3] = -majAxis - minAxis;\n    offsets[2] = majAxis - minAxis;\n\n    vec2 offset;\n    float w = gl_in[0].gl_Position.w;\n    for (int i = 0; i < 4; i++)\n    {\n        // transform offset back into clip space, and apply it to gl_Position.\n        offset = offsets[i];\n        offset.x *= (2.0f / WIDTH) * w;\n        offset.y *= (2.0f / HEIGHT) * w;\n\n        gl_Position = gl_in[0].gl_Position + vec4(offset.x, offset.y, 0.0, 0.0);\n        frag_color = geom_color[0];\n        frag_cov2inv = cov2Dinv4;\n        frag_p = geom_p[0];\n\n        EmitVertex();\n    }\n\n    EndPrimitive();\n}\n"
  },
  {
    "path": "shader/splat_peel_frag.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// 3d gaussian splat fragment shader\n//\n\n/*%%HEADER%%*/\n\nuniform sampler2D depthTex;\nuniform vec4 viewport;  // x, y, WIDTH, HEIGHT\n\nin vec4 frag_color;  // radiance of splat\nin vec4 frag_cov2inv;  // inverse of the 2D screen space covariance matrix of the guassian\nin vec2 frag_p;  // 2D screen space center of the guassian\n\nout vec4 out_color;\n\nvoid main()\n{\n    vec2 d = gl_FragCoord.xy - frag_p;\n\n    vec2 uv = gl_FragCoord.xy / viewport.zw;\n    float depth = texture(depthTex, uv).r;\n    if (gl_FragCoord.z <= depth)\n    {\n        discard;\n    }\n\n    // TODO: Use texture for gaussian evaluation\n    // evaluate the gaussian\n    mat2 cov2Dinv = mat2(frag_cov2inv.xy, frag_cov2inv.zw);\n    float g = exp(-0.5f * dot(d, cov2Dinv * d));\n\n    out_color.rgb = frag_color.a * g * frag_color.rgb;\n    out_color.a = frag_color.a * g;\n\n    if ((frag_color.a * g) <= (10.0f / 256.0f))\n    {\n        discard;\n    }\n\n    /*\n    // can be used to determine overdraw.\n    float epsilon = 1.0f / 256.0f;\n    out_color.rgb = frag_color.a * g * frag_color.rgb;\n    out_color.rgb = frag_color.rgb * 0.00000001f + vec3(epsilon, epsilon, epsilon);\n    out_color.a = 0.0f;\n    */\n}\n"
  },
  {
    "path": "shader/splat_vert.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n//\n// 3d gaussian splat vertex shader\n//\n\n/*%%HEADER%%*/\n\n/*%%DEFINES%%*/\n\nuniform mat4 viewMat;  // used to project position into view coordinates.\nuniform mat4 projMat;  // used to project view coordinates into clip coordinates.\nuniform vec4 projParams;  // x = HEIGHT / tan(FOVY / 2), y = Z_NEAR, z = Z_FAR\nuniform vec4 viewport;  // x, y, WIDTH, HEIGHT\nuniform vec3 eye;\n\nin vec4 position;  // center of the gaussian in object coordinates, (with alpha crammed in to w)\n\n// spherical harmonics coeff for radiance of the splat\nin vec4 r_sh0;  // sh coeff for red channel (up to third-order)\n#ifdef FULL_SH\nin vec4 r_sh1;\nin vec4 r_sh2;\nin vec4 r_sh3;\n#endif\nin vec4 g_sh0;  // sh coeff for green channel\n#ifdef FULL_SH\nin vec4 g_sh1;\nin vec4 g_sh2;\nin vec4 g_sh3;\n#endif\nin vec4 b_sh0;  // sh coeff for blue channel\n#ifdef FULL_SH\nin vec4 b_sh1;\nin vec4 b_sh2;\nin vec4 b_sh3;\n#endif\n\n// 3x3 covariance matrix of the splat in object coordinates.\nin vec3 cov3_col0;\nin vec3 cov3_col1;\nin vec3 cov3_col2;\n\nout vec4 geom_color;  // radiance of splat\nout vec4 geom_cov2;  // 2D screen space covariance matrix of the gaussian\nout vec2 geom_p;  // the 2D screen space center of the gaussian, (z is alpha)\n\nvec3 ComputeRadianceFromSH(const vec3 v)\n{\n#ifdef FULL_SH\n    float b[16];\n#else\n    float b[4];\n#endif\n\n    float vx2 = v.x * v.x;\n    float vy2 = v.y * v.y;\n    float vz2 = v.z * v.z;\n\n    // zeroth order\n    // (/ 1.0 (* 2.0 (sqrt pi)))\n    b[0] = 0.28209479177387814f;\n\n    // first order\n    // (/ (sqrt 3.0) (* 2 (sqrt pi)))\n    float k1 = 0.4886025119029199f;\n    b[1] = -k1 * v.y;\n    b[2] = k1 * v.z;\n    b[3] = -k1 * v.x;\n\n#ifdef FULL_SH\n    // second order\n    // (/ (sqrt 15.0) (* 2 (sqrt pi)))\n    float k2 = 1.0925484305920792f;\n    // (/ (sqrt 5.0) (* 4 (sqrt  pi)))\n    float k3 = 0.31539156525252005f;\n    // (/ (sqrt 15.0) (* 4 (sqrt pi)))\n    float k4 = 0.5462742152960396f;\n    b[4] = k2 * v.y * v.x;\n    b[5] = -k2 * v.y * v.z;\n    b[6] = k3 * (3.0f * vz2 - 1.0f);\n    b[7] = -k2 * v.x * v.z;\n    b[8] = k4 * (vx2 - vy2);\n\n    // third order\n    // (/ (* (sqrt 2) (sqrt 35)) (* 8 (sqrt pi)))\n    float k5 = 0.5900435899266435f;\n    // (/ (sqrt 105) (* 2 (sqrt pi)))\n    float k6 = 2.8906114426405543f;\n    // (/ (* (sqrt 2) (sqrt 21)) (* 8 (sqrt pi)))\n    float k7 = 0.4570457994644658f;\n    // (/ (sqrt 7) (* 4 (sqrt pi)))\n    float k8 = 0.37317633259011546f;\n    // (/ (sqrt 105) (* 4 (sqrt pi)))\n    float k9 = 1.4453057213202771f;\n    b[9] = -k5 * v.y * (3.0f * vx2 - vy2);\n    b[10] = k6 * v.y * v.x * v.z;\n    b[11] = -k7 * v.y * (5.0f * vz2 - 1.0f);\n    b[12] = k8 * v.z * (5.0f * vz2 - 3.0f);\n    b[13] = -k7 * v.x * (5.0f * vz2 - 1.0f);\n    b[14] = k9 * v.z * (vx2 - vy2);\n    b[15] = -k5 * v.x * (vx2 - 3.0f * vy2);\n\n    float re = (b[0] * r_sh0.x + b[1] * r_sh0.y + b[2] * r_sh0.z + b[3] * r_sh0.w +\n                b[4] * r_sh1.x + b[5] * r_sh1.y + b[6] * r_sh1.z + b[7] * r_sh1.w +\n                b[8] * r_sh2.x + b[9] * r_sh2.y + b[10]* r_sh2.z + b[11]* r_sh2.w +\n                b[12]* r_sh3.x + b[13]* r_sh3.y + b[14]* r_sh3.z + b[15]* r_sh3.w);\n\n    float gr = (b[0] * g_sh0.x + b[1] * g_sh0.y + b[2] * g_sh0.z + b[3] * g_sh0.w +\n                b[4] * g_sh1.x + b[5] * g_sh1.y + b[6] * g_sh1.z + b[7] * g_sh1.w +\n                b[8] * g_sh2.x + b[9] * g_sh2.y + b[10]* g_sh2.z + b[11]* g_sh2.w +\n                b[12]* g_sh3.x + b[13]* g_sh3.y + b[14]* g_sh3.z + b[15]* g_sh3.w);\n\n    float bl = (b[0] * b_sh0.x + b[1] * b_sh0.y + b[2] * b_sh0.z + b[3] * b_sh0.w +\n                b[4] * b_sh1.x + b[5] * b_sh1.y + b[6] * b_sh1.z + b[7] * b_sh1.w +\n                b[8] * b_sh2.x + b[9] * b_sh2.y + b[10]* b_sh2.z + b[11]* b_sh2.w +\n                b[12]* b_sh3.x + b[13]* b_sh3.y + b[14]* b_sh3.z + b[15]* b_sh3.w);\n#else\n    float re = (b[0] * r_sh0.x + b[1] * r_sh0.y + b[2] * r_sh0.z + b[3] * r_sh0.w);\n    float gr = (b[0] * g_sh0.x + b[1] * g_sh0.y + b[2] * g_sh0.z + b[3] * g_sh0.w);\n    float bl = (b[0] * b_sh0.x + b[1] * b_sh0.y + b[2] * b_sh0.z + b[3] * b_sh0.w);\n#endif\n    return vec3(0.5f, 0.5f, 0.5f) + vec3(re, gr, bl);\n}\n\n#ifdef FRAMEBUFFER_SRGB\nfloat SRGBToLinearF(float srgb)\n{\n    if (srgb <= 0.04045f)\n    {\n        return srgb / 12.92f;\n    }\n    else\n    {\n        return pow((srgb + 0.055f) / 1.055f, 2.4f);\n    }\n}\n\nvec3 SRGBToLinear(const vec3 srgbColor)\n{\n    vec3 linearColor;\n    for (int i = 0; i < 3; ++i) // Convert RGB, leave A unchanged\n    {\n        linearColor[i] = SRGBToLinearF(srgbColor[i]);\n    }\n    return linearColor;\n}\n#endif\n\nvoid main(void)\n{\n    // t is in view coordinates\n    float alpha = position.w;\n    vec4 t = viewMat * vec4(position.xyz, 1.0f);\n\n    //float X0 = viewport.x;\n    float X0 = viewport.x * (0.00001f * projParams.y);  // one weird hack to prevent projParams from being compiled away\n    float Y0 = viewport.y;\n    float WIDTH = viewport.z;\n    float HEIGHT = viewport.w;\n    float Z_NEAR = projParams.y;\n    float Z_FAR = projParams.z;\n\n    // J is the jacobian of the projection and viewport transformations.\n    // this is an affine approximation of the real projection.\n    // because gaussians are closed under affine transforms.\n    float SX = projMat[0][0];\n    float SY = projMat[1][1];\n    float WZ =  projMat[3][2];\n    float tzSq = t.z * t.z;\n    float jsx = -(SX * WIDTH) / (2.0f * t.z);\n    float jsy = -(SY * HEIGHT) / (2.0f * t.z);\n    float jtx = (SX * t.x * WIDTH) / (2.0f * tzSq);\n    float jty = (SY * t.y * HEIGHT) / (2.0f * tzSq);\n    float jtz = ((Z_FAR - Z_NEAR) * WZ) / (2.0f * tzSq);\n    mat3 J = mat3(vec3(jsx, 0.0f, 0.0f),\n                  vec3(0.0f, jsy, 0.0f),\n                  vec3(jtx, jty, jtz));\n\n    // combine the affine transforms of W (viewMat) and J (approx of viewportMat * projMat)\n    // using the fact that the new transformed covariance matrix V_Prime = JW * V * (JW)^T\n    mat3 W = mat3(viewMat);\n    mat3 V = mat3(cov3_col0, cov3_col1, cov3_col2);\n    mat3 JW = J * W;\n    mat3 V_prime = JW * V * transpose(JW);\n\n    // now we can 'project' the 3D covariance matrix onto the xy plane by just dropping the last column and row.\n    mat2 cov2D = mat2(V_prime);\n\n    // use the fact that the convolution of a gaussian with another gaussian is the sum\n    // of their covariance matrices to apply a low-pass filter to anti-alias the splats\n    cov2D[0][0] += 0.3f;\n    cov2D[1][1] += 0.3f;\n    geom_cov2 = vec4(cov2D[0], cov2D[1]); // cram it into a vec4\n\n    // geom_p is the gaussian center transformed into screen space\n    vec4 p4 = projMat * t;\n    geom_p = vec2(p4.x / p4.w, p4.y / p4.w);\n    geom_p.x = 0.5f * (WIDTH + (geom_p.x * WIDTH) + (2.0f * X0));\n    geom_p.y = 0.5f * (HEIGHT + (geom_p.y * HEIGHT) + (2.0f * Y0));\n\n    // compute radiance from sh\n    vec3 v = normalize(position.xyz - eye);\n    geom_color = vec4(ComputeRadianceFromSH(v), alpha);\n\n#ifdef FRAMEBUFFER_SRGB\n    // The SIBR reference renderer uses sRGB throughout,\n    // i.e. the splat colors are sRGB, the gaussian and alpha-blending occurs in sRGB space.\n    // However, in vr our shader output must be in linear space,\n    // in order for openxr color conversion to work.\n    // So, we convert the splat color to linear,\n    // but the guassian and alpha-blending occur in linear space.\n    // This leads to results that don't quite match the SIBR reference.\n    geom_color.rgb = SRGBToLinear(geom_color.rgb);\n#endif\n\n    // gl_Position is in clip coordinates.\n    gl_Position = p4;\n}\n"
  },
  {
    "path": "shader/text_frag.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n/*%%HEADER%%*/\n\nuniform sampler2D fontTex;\n\nin vec2 frag_uv;\nin vec4 frag_color;\n\nout vec4 out_color;\n\nvoid main()\n{\n    vec4 texColor = texture(fontTex, frag_uv, -0.5f); // bias to increase sharpness a bit\n\n    // premultiplied alpha blending\n    out_color.rgb = frag_color.a * frag_color.rgb * texColor.rgb;\n    out_color.a = frag_color.a * texColor.a;\n}\n"
  },
  {
    "path": "shader/text_vert.glsl",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n/*%%HEADER%%*/\n\nuniform mat4 modelViewProjMat;\n\nin vec3 position;\nin vec2 uv;\nin vec4 color;\n\nout vec2 frag_uv;\nout vec4 frag_color;\n\nvoid main(void)\n{\n    gl_Position = modelViewProjMat * vec4(position, 1.0f);\n    frag_uv = uv;\n    frag_color = color;\n}\n"
  },
  {
    "path": "src/android_main.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include <android/native_window_jni.h> // for native window JNI\n#include <android_native_app_glue.h>\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#include <jni.h>\n#include <sys/prctl.h> // for prctl( PR_SET_NAME )\n#include <sys/stat.h>\n#include <sys/types.h>\n\n#include \"core/log.h\"\n#include \"core/util.h\"\n#include \"core/xrbuddy.h\"\n#include \"app.h\"\n\n// AJT: ANDROID TODO: xrPerfSettingsSetPerformanceLevelEXT\n// AJT: ANDROID TODO: pfnSetAndroidApplicationThreadKHR on XR_SESSION_STATE_READY\n// see ovrApp::HandleSessionStateChanges in SceneModelXr.cpp\n/*\nstatic const int CPU_LEVEL = 2;\nstatic const int GPU_LEVEL = 3;\n*/\n\nstatic const char* EglErrorString(const EGLint error) {\n    switch (error) {\n        case EGL_SUCCESS:\n            return \"EGL_SUCCESS\";\n        case EGL_NOT_INITIALIZED:\n            return \"EGL_NOT_INITIALIZED\";\n        case EGL_BAD_ACCESS:\n            return \"EGL_BAD_ACCESS\";\n        case EGL_BAD_ALLOC:\n            return \"EGL_BAD_ALLOC\";\n        case EGL_BAD_ATTRIBUTE:\n            return \"EGL_BAD_ATTRIBUTE\";\n        case EGL_BAD_CONTEXT:\n            return \"EGL_BAD_CONTEXT\";\n        case EGL_BAD_CONFIG:\n            return \"EGL_BAD_CONFIG\";\n        case EGL_BAD_CURRENT_SURFACE:\n            return \"EGL_BAD_CURRENT_SURFACE\";\n        case EGL_BAD_DISPLAY:\n            return \"EGL_BAD_DISPLAY\";\n        case EGL_BAD_SURFACE:\n            return \"EGL_BAD_SURFACE\";\n        case EGL_BAD_MATCH:\n            return \"EGL_BAD_MATCH\";\n        case EGL_BAD_PARAMETER:\n            return \"EGL_BAD_PARAMETER\";\n        case EGL_BAD_NATIVE_PIXMAP:\n            return \"EGL_BAD_NATIVE_PIXMAP\";\n        case EGL_BAD_NATIVE_WINDOW:\n            return \"EGL_BAD_NATIVE_WINDOW\";\n        case EGL_CONTEXT_LOST:\n            return \"EGL_CONTEXT_LOST\";\n        default:\n            return \"unknown\";\n    }\n}\n\nstruct AppContext\n{\n    AppContext() : resumed(false), sessionActive(false), assMan(nullptr), alwaysCopyAssets(true) {}\n    bool resumed;\n    bool sessionActive;\n\n    struct EGLInfo\n    {\n        EGLInfo() : majorVersion(0), minorVersion(0), display(0), config(0), context(EGL_NO_CONTEXT) {}\n        EGLint majorVersion;\n        EGLint minorVersion;\n        EGLDisplay display;\n        EGLConfig config;\n        EGLContext context;\n        EGLSurface tinySurface;\n    };\n\n    EGLInfo egl;\n    AAssetManager* assMan;\n    std::string externalDataPath;\n    bool alwaysCopyAssets;\n\n    void Clear()\n    {\n        resumed = false;\n        sessionActive = false;\n        egl.majorVersion = 0;\n        egl.minorVersion = 0;\n        egl.display = 0;\n        egl.config = 0;\n        egl.context = EGL_NO_CONTEXT;\n    }\n\n    bool SetupEGLContext()\n    {\n        // create the egl context\n        egl.display = eglGetDisplay(EGL_DEFAULT_DISPLAY);\n        eglInitialize(egl.display, &egl.majorVersion, &egl.minorVersion);\n        Log::D(\"OpenGLES majorVersion = %d, minorVersion = %d\\n\", egl.majorVersion, egl.minorVersion);\n        const int MAX_CONFIGS = 1024;\n        EGLConfig configs[MAX_CONFIGS];\n        EGLint numConfigs = 0;\n        if (!eglGetConfigs(egl.display, configs, MAX_CONFIGS, &numConfigs))\n        {\n            Log::E(\"eglGetConfigs failed: %s\\n\", EglErrorString(eglGetError()));\n            return false;\n        }\n        const EGLint configAttribs[] = {EGL_RED_SIZE, 8,\n                                        EGL_GREEN_SIZE, 8,\n                                        EGL_BLUE_SIZE, 8,\n                                        EGL_ALPHA_SIZE, 8, // need alpha for the multi-pass timewarp compositor\n                                        EGL_DEPTH_SIZE, 0,\n                                        EGL_STENCIL_SIZE, 0,\n                                        EGL_SAMPLES, 0,\n                                        EGL_NONE};\n        egl.config = 0;\n        for (int i = 0; i < numConfigs; i++)\n        {\n            EGLint value = 0;\n            eglGetConfigAttrib(egl.display, configs[i], EGL_RENDERABLE_TYPE, &value);\n            if ((value & EGL_OPENGL_ES3_BIT_KHR) != EGL_OPENGL_ES3_BIT_KHR) {\n                continue;\n            }\n            // The pbuffer config also needs to be compatible with normal window rendering\n            // so it can share textures with the window context.\n            eglGetConfigAttrib(egl.display, configs[i], EGL_SURFACE_TYPE, &value);\n            if ((value & (EGL_WINDOW_BIT | EGL_PBUFFER_BIT)) != (EGL_WINDOW_BIT | EGL_PBUFFER_BIT)) {\n                continue;\n            }\n\n            int j = 0;\n            for (; configAttribs[j] != EGL_NONE; j += 2) {\n                eglGetConfigAttrib(egl.display, configs[i], configAttribs[j], &value);\n                if (value != configAttribs[j + 1]) {\n                    break;\n                }\n            }\n            if (configAttribs[j] == EGL_NONE) {\n                egl.config = configs[i];\n                break;\n            }\n        }\n\n        if (egl.config == 0)\n        {\n            Log::E(\"eglChooseConfig() failed: %s\\n\", EglErrorString(eglGetError()));\n            return false;\n        }\n\n        EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};\n        egl.context = eglCreateContext(egl.display, egl.config, EGL_NO_CONTEXT, contextAttribs);\n        if (egl.context == EGL_NO_CONTEXT)\n        {\n            Log::E(\"eglCreateContext() failed: %s\", EglErrorString(eglGetError()));\n            return false;\n        }\n\n        const EGLint surfaceAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};\n        egl.tinySurface = eglCreatePbufferSurface(egl.display, egl.config, surfaceAttribs);\n        if (egl.tinySurface == EGL_NO_SURFACE)\n        {\n            Log::E(\"eglCreatePbufferSurface() failed: %s\", EglErrorString(eglGetError()));\n            eglDestroyContext(egl.display, egl.context);\n            egl.context = EGL_NO_CONTEXT;\n            return false;\n        }\n\n        if (eglMakeCurrent(egl.display, egl.tinySurface, egl.tinySurface, egl.context) == EGL_FALSE)\n        {\n            Log::E(\"eglMakeCurrent() failed: %s\", EglErrorString(eglGetError()));\n            eglDestroySurface(egl.display, egl.tinySurface);\n            eglDestroyContext(egl.display, egl.context);\n            egl.context = EGL_NO_CONTEXT;\n            return false;\n        }\n\n        return true;\n    }\n\n    bool SetupAssets(android_app* app)\n    {\n        assert(app);\n        assert(app->activity->assetManager);\n        assert(app->activity->externalDataPath);\n\n        assMan = app->activity->assetManager;\n        externalDataPath = std::string(app->activity->externalDataPath) + \"/\";\n\n        // from util.h\n        SetRootPath(externalDataPath);\n\n        Log::D(\"AJT: externalDataPath = \\\"%s\\\"\\n\", externalDataPath.c_str());\n\n        MakeDir(\"texture\");\n        UnpackAsset(\"texture/carpet.png\");\n        UnpackAsset(\"texture/sphere.png\");\n\n        MakeDir(\"shader\");\n        UnpackAsset(\"shader/carpet_frag.glsl\");\n        UnpackAsset(\"shader/carpet_vert.glsl\");\n        UnpackAsset(\"shader/debugdraw_frag.glsl\");\n        UnpackAsset(\"shader/debugdraw_vert.glsl\");\n        UnpackAsset(\"shader/desktop_frag.glsl\");\n        UnpackAsset(\"shader/desktop_vert.glsl\");\n        UnpackAsset(\"shader/point_frag.glsl\");\n        UnpackAsset(\"shader/point_geom.glsl\");\n        UnpackAsset(\"shader/point_vert.glsl\");\n        UnpackAsset(\"shader/presort_compute.glsl\");\n        UnpackAsset(\"shader/splat_frag.glsl\");\n        UnpackAsset(\"shader/splat_geom.glsl\");\n        UnpackAsset(\"shader/splat_vert.glsl\");\n        UnpackAsset(\"shader/text_frag.glsl\");\n        UnpackAsset(\"shader/text_vert.glsl\");\n\n        MakeDir(\"font\");\n        UnpackAsset(\"font/JetBrainsMono-Medium.json\");\n        UnpackAsset(\"font/JetBrainsMono-Medium.png\");\n\n        MakeDir(\"data\");\n        MakeDir(\"data/sh_test\");\n        UnpackAsset(\"data/sh_test/cameras.json\");\n        UnpackAsset(\"data/sh_test/cfg_args\");\n        UnpackAsset(\"data/sh_test/input.ply\");\n        MakeDir(\"data/sh_test/point_cloud\");\n        MakeDir(\"data/sh_test/point_cloud/iteration_30000\");\n        UnpackAsset(\"data/sh_test/point_cloud/iteration_30000/point_cloud.ply\");\n        UnpackAsset(\"data/sh_test/vr.json\");\n\n        MakeDir(\"data/livingroom\");\n        UnpackAsset(\"data/livingroom/livingroom.ply\");\n        UnpackAsset(\"data/livingroom/livingroom_vr.json\");\n\n        return true;\n    }\n\n    bool MakeDir(const std::string& dirFilename)\n    {\n        std::string filename = externalDataPath + dirFilename;\n        if (mkdir(filename.c_str(), 0777) != 0)\n        {\n            if (errno == EEXIST)\n            {\n                Log::D(\"MakeDir \\\"%s\\\" already exists\\n\", dirFilename.c_str());\n                // dir already exists!\n                return true;\n            }\n            else\n            {\n                Log::E(\"mkdir failed on dir \\\"%s\\\" errno = %d\\n\", filename.c_str(), errno);\n                return false;\n            }\n        }\n        Log::D(\"MakeDir \\\"%s\\\"\\n\", dirFilename.c_str());\n\n        return true;\n    }\n\n    bool UnpackAsset(const std::string& assetFilename)\n    {\n        std::string outputFilename = externalDataPath + assetFilename;\n\n        struct stat sb;\n        if (stat(outputFilename.c_str(), &sb) == 0)\n        {\n            if (!alwaysCopyAssets)\n            {\n                Log::D(\"UnpackAsset \\\"%s\\\" already exists\\n\", assetFilename.c_str());\n                return true;\n            }\n        }\n\n        AAsset *asset = AAssetManager_open(assMan, assetFilename.c_str(), AASSET_MODE_STREAMING);\n        if (asset == nullptr)\n        {\n            Log::E(\"UnpackAsset \\\"%s\\\" AAssetManager_open failed!\\n\", assetFilename.c_str());\n            return false; // Failed to open the asset\n        }\n\n        // Create buffer for reading\n        const size_t BUFFER_SIZE = 1024;\n        char buffer[BUFFER_SIZE];\n\n        // Open file for writing\n        FILE *outFile = fopen(outputFilename.c_str(), \"w\");\n        if (outFile == nullptr)\n        {\n            Log::E(\"UnpackAsset \\\"%s\\\" fopen failed!\\n\", assetFilename.c_str());\n            AAsset_close(asset);\n            return false; // Failed to open the output file\n        }\n\n        // Read from assets and write to file\n        int bytesRead;\n        while ((bytesRead = AAsset_read(asset, buffer, BUFFER_SIZE)) > 0)\n        {\n            fwrite(buffer, sizeof(char), bytesRead, outFile);\n        }\n\n        // Close the asset and the output file\n        AAsset_close(asset);\n        fclose(outFile);\n\n        Log::D(\"UnpackAsset \\\"%s\\\"\\n\", assetFilename.c_str());\n        return true;\n    }\n};\n\n/**\n * Process the next main command.\n */\nstatic void app_handle_cmd(struct android_app* androidApp, int32_t cmd)\n{\n    AppContext& ctx = *(AppContext*)androidApp->userData;\n\n    switch (cmd)\n    {\n    // There is no APP_CMD_CREATE. The ANativeActivity creates the\n    // application thread from onCreate(). The application thread\n    // then calls android_main().\n    case APP_CMD_START:\n        Log::D(\"onStart()\\n\");\n        Log::D(\"    APP_CMD_START\\n\");\n        break;\n    case APP_CMD_RESUME:\n        Log::D(\"onResume()\\n\");\n        Log::D(\"    APP_CMD_RESUME\\n\");\n        ctx.resumed = true;\n        break;\n    case APP_CMD_PAUSE:\n        Log::D(\"onPause()\\n\");\n        Log::D(\"    APP_CMD_PAUSE\\n\");\n        ctx.resumed = false;\n        break;\n    case APP_CMD_STOP:\n        Log::D(\"onStop()\\n\");\n        Log::D(\"    APP_CMD_STOP\\n\");\n        break;\n    case APP_CMD_DESTROY:\n        Log::D(\"onDestroy()\\n\");\n        Log::D(\"    APP_CMD_DESTROY\\n\");\n        ctx.Clear();\n        break;\n    case APP_CMD_INIT_WINDOW:\n        Log::D(\"surfaceCreated()\\n\");\n        Log::D(\"    APP_CMD_INIT_WINDOW\\n\");\n        break;\n    case APP_CMD_TERM_WINDOW:\n        Log::D(\"surfaceDestroyed()\\n\");\n        Log::D(\"    APP_CMD_TERM_WINDOW\\n\");\n        break;\n    }\n}\n\n/**\n * This is the main entry point of a native application that is using\n * android_native_app_glue.  It runs in its own thread, with its own\n * event loop for receiving input events and doing other things.\n */\nvoid android_main(struct android_app* androidApp)\n{\n    Log::SetAppName(\"splatapult\");\n\n    Log::D(\"----------------------------------------------------------------\\n\");\n    Log::D(\"android_app_entry()\\n\");\n    Log::D(\"    android_main()\\n\");\n\n    JNIEnv* env;\n    (*androidApp->activity->vm).AttachCurrentThread(&env, nullptr);\n\n    // Note that AttachCurrentThread will reset the thread name.\n    prctl(PR_SET_NAME, (long)\"android_main\", 0, 0, 0);\n\n    AppContext ctx;\n    androidApp->userData = &ctx;\n    androidApp->onAppCmd = app_handle_cmd;\n\n    if (!ctx.SetupEGLContext())\n    {\n        Log::E(\"AppContext::SetupEGLContext failed!\\n\");\n        return;\n    }\n\n    if (!ctx.SetupAssets(androidApp))\n    {\n        Log::E(\"AppContext::SetupAssets failed!\\n\");\n        return;\n    }\n\n    MainContext mainContext;\n    mainContext.display = ctx.egl.display;\n    mainContext.config = ctx.egl.config;\n    mainContext.context = ctx.egl.context;\n    mainContext.androidApp = androidApp;\n\n    std::string dataPath = ctx.externalDataPath + \"data/livingroom/livingroom.ply\";\n    int argc = 4;\n    const char* argv[] = {\"splataplut\", \"-v\", \"-d\", dataPath.c_str()};\n    App app(mainContext);\n    App::ParseResult parseResult = app.ParseArguments(argc, argv);\n    switch (parseResult)\n    {\n    case App::SUCCESS_RESULT:\n        break;\n    case App::ERROR_RESULT:\n        Log::E(\"App::ParseArguments failed!\\n\");\n        return;\n    case App::QUIT_RESULT:\n        return;\n    }\n\n    if (!app.Init())\n    {\n        Log::E(\"App::Init failed!\\n\");\n        return;\n    }\n\n    while (androidApp->destroyRequested == 0)\n    {\n        // Read all pending events.\n        for (;;)\n        {\n            int events;\n            struct android_poll_source* source;\n            // If the timeout is zero, returns immediately without blocking.\n            // If the timeout is negative, waits indefinitely until an event appears.\n            int timeoutMilliseconds = 0;\n            if (ctx.resumed == false && ctx.sessionActive == false && androidApp->destroyRequested == 0)\n            {\n                timeoutMilliseconds = -1;\n            }\n\n            if (ALooper_pollAll(timeoutMilliseconds, NULL, &events, (void**)&source) < 0)\n            {\n                break;\n            }\n\n            // Process this event.\n            if (source != NULL)\n            {\n                source->process(androidApp, source);\n            }\n        }\n\n        float dt = 1.0f / 72.0f;\n        if (!app.Process(dt))\n        {\n            Log::E(\"App::Process failed!\\n\");\n            break;\n        }\n\n        if (!app.Render(dt, glm::ivec2(0.0f, 0.0f)))\n        {\n            Log::E(\"App::Render failed!\\n\");\n            return;\n        }\n    }\n\n    // TODO: DESTROY STUFF\n    Log::D(\"Finished!\\n\");\n\n    (*androidApp->activity->vm).DetachCurrentThread();\n}\n"
  },
  {
    "path": "src/app.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"app.h\"\n\n#ifndef __ANDROID__\n#define USE_SDL\n#include <GL/glew.h>\n#endif\n\n#ifdef USE_SDL\n#include <SDL2/SDL.h>\n#endif\n\n#include <filesystem>\n#include <thread>\n\n#ifdef TRACY_ENABLE\n#include <tracy/Tracy.hpp>\n#else\n#define ZoneScoped\n#define ZoneScopedNC(NAME, COLOR)\n#endif\n\n#include \"core/framebuffer.h\"\n#include \"core/log.h\"\n#include \"core/debugrenderer.h\"\n#include \"core/inputbuddy.h\"\n#include \"core/optionparser.h\"\n#include \"core/textrenderer.h\"\n#include \"core/texture.h\"\n#include \"core/util.h\"\n#include \"core/xrbuddy.h\"\n\n#include \"camerasconfig.h\"\n#include \"camerapathrenderer.h\"\n#include \"flycam.h\"\n#include \"gaussiancloud.h\"\n#include \"magiccarpet.h\"\n#include \"pointcloud.h\"\n#include \"pointrenderer.h\"\n#include \"splatrenderer.h\"\n#include \"vrconfig.h\"\n\nenum optionIndex\n{\n    UNKNOWN,\n    OPENXR,\n    FULLSCREEN,\n    DEBUG,\n    HELP,\n    FP16,\n    FP32,\n    NOSH,\n};\n\nconst option::Descriptor usage[] =\n{\n    { UNKNOWN, 0, \"\", \"\", option::Arg::None, \"USAGE: splatapult [options] FILE.ply\\n\\nOptions:\" },\n    { HELP, 0, \"h\", \"help\", option::Arg::None,            \"  -h, --help        Print usage and exit.\" },\n    { OPENXR, 0, \"v\", \"openxr\", option::Arg::None,        \"  -v, --openxr      Launch app in vr mode, using openxr runtime.\" },\n    { FULLSCREEN, 0, \"f\", \"fullscren\", option::Arg::None, \"  -f, --fullscreen  Launch window in fullscreen.\" },\n    { DEBUG, 0, \"d\", \"debug\", option::Arg::None,          \"  -d, --debug       Enable verbose debug logging.\" },\n    { FP16, 0, \"\", \"fp16\", option::Arg::None,             \"  --fp16            Use 16-bit half-precision floating frame buffer, to reduce color banding artifacts\" },\n    { FP32, 0, \"\", \"fp32\", option::Arg::None,             \"  --fp32            Use 32-bit floating point frame buffer, to reduce color banding even more\" },\n    { NOSH, 0, \"\", \"nosh\", option::Arg::None,             \"  --nosh            Don't load/render full sh, this will reduce memory usage and higher performance\" },\n    { UNKNOWN, 0, \"\", \"\", option::Arg::None,              \"\\nExamples:\\n  splataplut data/test.ply\\n  splatapult -v data/test.ply\" },\n    { 0, 0, 0, 0, 0, 0}\n};\n\nconst float Z_NEAR = 0.1f;\nconst float Z_FAR = 1000.0f;\nconst float FOVY = glm::radians(45.0f);\n\nconst float MOVE_SPEED = 2.5f;\nconst float ROT_SPEED = 1.15f;\n\nconst glm::vec4 WHITE = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);\nconst glm::vec4 BLACK = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);\nconst int TEXT_NUM_ROWS = 25;\n\n#include <string>\n#include <filesystem>\n#include <iostream>\n\n// searches for file named configFilename, dir that contains plyFilename, it's parent and grandparent dirs.\nstatic std::string FindConfigFile(const std::string& plyFilename, const std::string& configFilename)\n{\n    std::filesystem::path plyPath(plyFilename);\n\n    if (!std::filesystem::exists(plyPath) || !std::filesystem::is_regular_file(plyPath))\n    {\n        Log::E(\"PLY file does not exist or is not a file: \\\"%s\\\"\", plyFilename.c_str());\n        return \"\";\n    }\n\n    std::filesystem::path directory = plyPath.parent_path();\n\n    for (int i = 0; i < 3; ++i) // Check current, parent, and grandparent directories\n    {\n        std::filesystem::path configPath = directory / configFilename;\n        if (std::filesystem::exists(configPath) && std::filesystem::is_regular_file(configPath))\n        {\n            return configPath.string();\n        }\n        if (directory.has_parent_path())\n        {\n            directory = directory.parent_path();\n        }\n        else\n        {\n            break;\n        }\n    }\n\n    return \"\";\n}\n\nstatic std::string GetFilenameWithoutExtension(const std::string& filepath)\n{\n    std::filesystem::path pathObj(filepath);\n\n    // Check if the path has a stem (the part of the path before the extension)\n    if (pathObj.has_stem())\n    {\n        return pathObj.stem().string();\n    }\n\n    // If there is no stem, return an empty string\n    return \"\";\n}\n\nstatic std::string MakeVrConfigFilename(const std::string& plyFilename)\n{\n    std::filesystem::path plyPath(plyFilename);\n    std::filesystem::path directory = plyPath.parent_path();\n    std::string plyNoExt = GetFilenameWithoutExtension(plyFilename) + \"_vr.json\";\n    std::filesystem::path configPath = directory / plyNoExt;\n    return configPath.string();\n}\n\nstatic void Clear(glm::ivec2 windowSize, bool setViewport = true)\n{\n    int width = windowSize.x;\n    int height = windowSize.y;\n    if (setViewport)\n    {\n        glViewport(0, 0, width, height);\n    }\n\n    // pre-multiplied alpha blending\n    glEnable(GL_BLEND);\n    glBlendEquation(GL_FUNC_ADD);\n    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);\n\n    glm::vec4 clearColor(0.0f, 0.0f, 0.0f, 1.0f);\n    glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);\n    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n\n    // NOTE: if depth buffer has less then 24 bits, it can mess up splat rendering.\n    glEnable(GL_DEPTH_TEST);\n}\n\n// Draw a textured quad over the entire screen.\nstatic void RenderDesktop(glm::ivec2 windowSize, std::shared_ptr<Program> desktopProgram, uint32_t colorTexture, bool adjustAspect)\n{\n    int width = windowSize.x;\n    int height = windowSize.y;\n\n    glViewport(0, 0, width, height);\n    glm::vec4 clearColor(0.0f, 0.0f, 0.0f, 1.0f);\n    glClearColor(clearColor.x, clearColor.y, clearColor.z, clearColor.w);\n    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n    glm::mat4 projMat = glm::ortho(0.0f, (float)width, 0.0f, (float)height, -10.0f, 10.0f);\n\n    if (colorTexture > 0)\n    {\n        desktopProgram->Bind();\n        desktopProgram->SetUniform(\"modelViewProjMat\", projMat);\n        desktopProgram->SetUniform(\"color\", glm::vec4(1.0f));\n\n        // use texture unit 0 for colorTexture\n        glActiveTexture(GL_TEXTURE0);\n        glBindTexture(GL_TEXTURE_2D, colorTexture);\n        desktopProgram->SetUniform(\"colorTexture\", 0);\n\n        glm::vec2 xyLowerLeft(0.0f, 0.0f);\n        glm::vec2 xyUpperRight((float)width, (float)height);\n        if (adjustAspect)\n        {\n            xyLowerLeft = glm::vec2(0.0f, (height - width) / 2.0f);\n            xyUpperRight = glm::vec2((float)width, (height + width) / 2.0f);\n        }\n        glm::vec2 uvLowerLeft(0.0f, 0.0f);\n        glm::vec2 uvUpperRight(1.0f, 1.0f);\n\n        float depth = -9.0f;\n        glm::vec3 positions[] = {glm::vec3(xyLowerLeft, depth), glm::vec3(xyUpperRight.x, xyLowerLeft.y, depth),\n                                 glm::vec3(xyUpperRight, depth), glm::vec3(xyLowerLeft.x, xyUpperRight.y, depth)};\n        desktopProgram->SetAttrib(\"position\", positions);\n\n        glm::vec2 uvs[] = {uvLowerLeft, glm::vec2(uvUpperRight.x, uvLowerLeft.y),\n                           uvUpperRight, glm::vec2(uvLowerLeft.x, uvUpperRight.y)};\n        desktopProgram->SetAttrib(\"uv\", uvs);\n\n        const size_t NUM_INDICES = 6;\n        uint16_t indices[NUM_INDICES] = {0, 1, 2, 0, 2, 3};\n        glDrawElements(GL_TRIANGLES, NUM_INDICES, GL_UNSIGNED_SHORT, indices);\n    }\n}\n\nstatic std::shared_ptr<PointCloud> LoadPointCloud(const std::string& plyFilename, bool useLinearColors)\n{\n    auto pointCloud = std::make_shared<PointCloud>(useLinearColors);\n\n    if (!pointCloud->ImportPly(plyFilename))\n    {\n        Log::E(\"Error loading PointCloud!\\n\");\n        return nullptr;\n    }\n    return pointCloud;\n}\n\nstatic std::shared_ptr<GaussianCloud> LoadGaussianCloud(const std::string& plyFilename, const App::Options& opt)\n{\n    GaussianCloud::Options options = {0};\n#ifdef __ANDROID__\n    options.importFullSH = false;\n    options.exportFullSH = false;\n#else\n    options.importFullSH = opt.importFullSH;\n    options.exportFullSH = true;\n#endif\n    auto gaussianCloud = std::make_shared<GaussianCloud>(options);\n    if (!gaussianCloud->ImportPly(plyFilename))\n    {\n        Log::E(\"Error loading GaussianCloud!\\n\");\n        return nullptr;\n    }\n\n    return gaussianCloud;\n}\n\nstatic void PrintControls()\n{\n    fprintf(stdout, \"\\\n\\n\\\nDesktop Controls\\n\\\n--------------------\\n\\\n* wasd - move\\n\\\n* arrow keys - look\\n\\\n* right mouse button - hold down for mouse look.\\n\\\n* gamepad - if present, right stick to rotate, left stick to move, bumpers to roll\\n\\\n* c - toggle between initial SfM point cloud (if present) and gaussian splats.\\n\\\n* n - jump to next camera\\n\\\n* p - jump to previous camera\\n\\\n\\n\\\nVR Controls\\n\\\n---------------\\n\\\n* c - toggle between initial SfM point cloud (if present) and gaussian splats.\\n\\\n* left stick - move\\n\\\n* right stick - snap turn\\n\\\n* f - show hide floor carpet.\\n\\\n* single grab - translate the world.\\n\\\n* double grab - rotate and translate the world.\\n\\\n* triple grab - (double grab while trigger is depressed) scale, rotate and translate the world.\\n\\\n* return - save the current position and orientation/scale of the world into a vr.json file.\\n\\\n\\n\");\n}\n\nApp::App(MainContext& mainContextIn):\n    mainContext(mainContextIn)\n{\n    cameraIndex = 0;\n    virtualLeftStick = glm::vec2(0.0f, 0.0f);\n    virtualRightStick = glm::vec2(0.0f, 0.0f);\n    mouseLookStick = glm::vec2(0.0f, 0.0f);\n    mouseLook = false;\n    virtualRoll = 0.0f;\n    virtualUp = 0.0f;\n    frameNum = 0;\n}\n\nApp::ParseResult App::ParseArguments(int argc, const char* argv[])\n{\n    // skip program name\n    if (argc > 0)\n    {\n        argc--;\n        argv++;\n    }\n    option::Stats stats(usage, argc, argv);\n    std::vector<option::Option> options(stats.options_max);\n    std::vector<option::Option> buffer(stats.buffer_max);\n    option::Parser parse(usage, argc, argv, options.data(), buffer.data());\n\n    if (parse.error())\n    {\n        return ERROR_RESULT;\n    }\n\n    if (options[HELP] || argc == 0)\n    {\n        option::printUsage(std::cout, usage);\n        PrintControls();\n        return QUIT_RESULT;\n    }\n\n    if (options[OPENXR])\n    {\n        opt.vrMode = true;\n    }\n\n    if (options[FULLSCREEN])\n    {\n        opt.fullscreen = true;\n    }\n\n    if (options[DEBUG])\n    {\n        opt.debugLogging = true;\n    }\n\n    if (options[FP32])\n    {\n        opt.frameBuffer = Options::FrameBuffer::Float;\n    }\n    else if (options[FP16])\n    {\n        opt.frameBuffer = Options::FrameBuffer::HalfFloat;\n    }\n\n    opt.importFullSH = options[NOSH] ? false : true;\n\n    bool unknownOptionFound = false;\n    for (option::Option* opt = options[UNKNOWN]; opt; opt = opt->next())\n    {\n        unknownOptionFound = true;\n        std::cout << \"Unknown option: \" << std::string(opt->name,opt->namelen) << \"\\n\";\n    }\n    if (unknownOptionFound)\n    {\n        return ERROR_RESULT;\n    }\n\n    if (parse.nonOptionsCount() == 0)\n    {\n        std::cout << \"Expected filename argument\\n\";\n        return ERROR_RESULT;\n    }\n    else\n    {\n        plyFilename = parse.nonOption(0);\n    }\n\n    Log::SetLevel(opt.debugLogging ? Log::Debug : Log::Warning);\n\n    std::filesystem::path plyPath(plyFilename);\n    if (!std::filesystem::exists(plyPath) || !std::filesystem::is_regular_file(plyPath))\n    {\n        Log::E(\"Invalid file \\\"%s\\\"\\n\", plyFilename.c_str());\n        return ERROR_RESULT;\n    }\n\n    return SUCCESS_RESULT;\n}\n\nbool App::Init()\n{\n    bool isFramebufferSRGBEnabled = opt.vrMode;\n\n#ifndef __ANDROID__\n    // AJT: ANDROID: TODO: make sure colors are accurate on android.\n    if (isFramebufferSRGBEnabled)\n    {\n        // necessary for proper color conversion\n        glEnable(GL_FRAMEBUFFER_SRGB);\n    }\n    else\n    {\n        glDisable(GL_FRAMEBUFFER_SRGB);\n    }\n\n    GLenum err = glewInit();\n    if (GLEW_OK != err)\n    {\n        Log::E(\"Error: %s\\n\", glewGetErrorString(err));\n        return false;\n    }\n#endif\n\n    debugRenderer = std::make_shared<DebugRenderer>();\n    if (!debugRenderer->Init())\n    {\n        Log::E(\"DebugRenderer Init failed\\n\");\n        return false;\n    }\n\n    textRenderer = std::make_shared<TextRenderer>();\n    if (!textRenderer->Init(\"font/JetBrainsMono-Medium.json\", \"font/JetBrainsMono-Medium.png\"))\n    {\n        Log::E(\"TextRenderer Init failed\\n\");\n        return false;\n    }\n\n    if (opt.vrMode)\n    {\n        xrBuddy = std::make_shared<XrBuddy>(mainContext, glm::vec2(Z_NEAR, Z_FAR));\n        if (!xrBuddy->Init())\n        {\n            Log::E(\"OpenXR Init failed\\n\");\n            return false;\n        }\n    }\n\n    std::string camerasConfigFilename = FindConfigFile(plyFilename, \"cameras.json\");\n    if (!camerasConfigFilename.empty())\n    {\n        camerasConfig = std::make_shared<CamerasConfig>();\n        if (!camerasConfig->ImportJson(camerasConfigFilename))\n        {\n            Log::W(\"Error loading cameras.json\\n\");\n            camerasConfig.reset();\n        }\n    }\n    else\n    {\n        Log::D(\"Could not find cameras.json\\n\");\n    }\n\n    if (camerasConfig)\n    {\n        cameraPathRenderer = std::make_shared<CameraPathRenderer>();\n        if (!cameraPathRenderer->Init(camerasConfig->GetCameraVec()))\n        {\n            Log::E(\"CameraPathRenderer Init failed\\n\");\n            return false;\n        }\n    }\n\n    // search for vr config file\n    // for example: if plyFilename is \"input.ply\", then search for \"input_vr.json\"\n    std::string vrConfigBaseFilename = GetFilenameWithoutExtension(plyFilename) + \"_vr.json\";\n    std::string vrConfigFilename = FindConfigFile(plyFilename, vrConfigBaseFilename);\n    if (!vrConfigFilename.empty())\n    {\n        vrConfig = std::make_shared<VrConfig>();\n        if (!vrConfig->ImportJson(vrConfigFilename))\n        {\n            Log::I(\"Could not load vr.json\\n\");\n            vrConfig.reset();\n        }\n    }\n    else\n    {\n        Log::D(\"Could not find %s\\n\", vrConfigFilename.c_str());\n        // Where we'd like the vr config file to exist.\n        vrConfigFilename = MakeVrConfigFilename(plyFilename);\n    }\n\n    glm::mat4 flyCamMat(1.0f);\n    glm::mat4 floorMat(1.0f);\n    if (camerasConfig)\n    {\n        flyCamMat = camerasConfig->GetCameraVec()[cameraIndex].mat;\n\n        // initialize magicCarpet from first camera and estimated floor position.\n        if (camerasConfig->GetNumCameras() > 0)\n        {\n            glm::vec3 floorNormal, floorPos;\n            camerasConfig->EstimateFloorPlane(floorNormal, floorPos);\n            glm::vec3 floorZ = camerasConfig->GetCameraVec()[0].mat[2];\n            glm::vec3 floorY = floorNormal;\n            glm::vec3 floorX = glm::cross(floorY, floorZ);\n            floorZ = glm::cross(floorX, floorY);\n\n            floorMat = glm::mat4(glm::vec4(floorX, 0.0f),\n                                 glm::vec4(floorY, 0.0f),\n                                 glm::vec4(floorZ, 0.0f),\n                                 glm::vec4(floorPos, 1.0f));\n        }\n    }\n\n    if (vrConfig)\n    {\n        floorMat = vrConfig->GetFloorMat();\n\n        if (!camerasConfig)\n        {\n            glm::vec3 pos = floorMat[3];\n            pos += glm::mat3(floorMat) * glm::vec3(0.0f, 1.5f, 0.0f);\n            glm::mat4 adjustedFloorMat = floorMat;\n            adjustedFloorMat[3] = glm::vec4(pos, 1.0f);\n            flyCamMat = adjustedFloorMat;\n        }\n    }\n\n    glm::vec3 flyCamPos, flyCamScale, floorMatUp;\n    glm::quat flyCamRot;\n    floorMatUp = glm::vec3(floorMat[1]);\n    Decompose(flyCamMat, &flyCamScale, &flyCamRot, &flyCamPos);\n    flyCam = std::make_shared<FlyCam>(floorMatUp, flyCamPos, flyCamRot, MOVE_SPEED, ROT_SPEED);\n\n    magicCarpet = std::make_shared<MagicCarpet>(floorMat, MOVE_SPEED);\n    if (!magicCarpet->Init(isFramebufferSRGBEnabled))\n    {\n        Log::E(\"Error initalizing MagicCarpet\\n\");\n        return false;\n    }\n\n    std::string pointCloudFilename = FindConfigFile(plyFilename, \"input.ply\");\n    if (!pointCloudFilename.empty())\n    {\n        pointCloud = LoadPointCloud(pointCloudFilename, isFramebufferSRGBEnabled);\n        if (!pointCloud)\n        {\n            Log::E(\"Error loading PointCloud\\n\");\n            return false;\n        }\n\n        pointRenderer = std::make_shared<PointRenderer>();\n        if (!pointRenderer->Init(pointCloud, isFramebufferSRGBEnabled))\n        {\n            Log::E(\"Error initializing point renderer!\\n\");\n            return false;\n        }\n    }\n    else\n    {\n        Log::D(\"Could not find input.ply\\n\");\n    }\n\n    gaussianCloud = LoadGaussianCloud(plyFilename, opt);\n    if (!gaussianCloud)\n    {\n        Log::E(\"Error loading GaussianCloud\\n\");\n        return false;\n    }\n\n#if 0\n    const uint32_t SPLAT_COUNT = 25000;\n    glm::vec3 focalPoint = flyCam->GetCameraMat()[3];\n    //gaussianCloud->PruneSplats(glm::vec3(flyCam->GetCameraMat()[3]), SPLAT_COUNT);\n    gaussianCloud->PruneSplats(focalPoint, SPLAT_COUNT);\n#endif\n\n    splatRenderer = std::make_shared<SplatRenderer>();\n#if __ANDROID__\n    bool useRgcSortOverride = true;\n#else\n    bool useRgcSortOverride = false;\n#endif\n    if (!splatRenderer->Init(gaussianCloud, isFramebufferSRGBEnabled, useRgcSortOverride))\n    {\n        Log::E(\"Error initializing splat renderer!\\n\");\n        return false;\n    }\n\n    if (opt.vrMode)\n    {\n        desktopProgram = std::make_shared<Program>();\n        std::string defines = \"#define USE_SUPERSAMPLING\\n\";\n        desktopProgram->AddMacro(\"DEFINES\", defines);\n        if (!desktopProgram->LoadVertFrag(\"shader/desktop_vert.glsl\", \"shader/desktop_frag.glsl\"))\n        {\n            Log::E(\"Error loading desktop shader!\\n\");\n            return 1;\n        }\n\n        xrBuddy->SetRenderCallback([this](\n            const glm::mat4& projMat, const glm::mat4& eyeMat,\n            const glm::vec4& viewport, const glm::vec2& nearFar, int viewNum)\n        {\n            Clear(glm::ivec2(0, 0), false);\n\n            glm::mat4 fullEyeMat = magicCarpet->GetCarpetMat() * eyeMat;\n\n            if (opt.drawDebug)\n            {\n                debugRenderer->Render(fullEyeMat, projMat, viewport, nearFar);\n            }\n\n            if (cameraPathRenderer)\n            {\n                cameraPathRenderer->SetShowCameras(opt.drawCameraFrustums);\n                cameraPathRenderer->SetShowPath(opt.drawCameraPath);\n                cameraPathRenderer->Render(fullEyeMat, projMat, viewport, nearFar);\n            }\n\n            if (opt.drawCarpet)\n            {\n                magicCarpet->Render(fullEyeMat, projMat, viewport, nearFar);\n            }\n\n            if (opt.drawPointCloud && pointRenderer)\n            {\n                pointRenderer->Render(fullEyeMat, projMat, viewport, nearFar);\n            }\n            else\n            {\n                if (viewNum == 0)\n                {\n                    splatRenderer->Sort(fullEyeMat, projMat, viewport, nearFar);\n                }\n                splatRenderer->Render(fullEyeMat, projMat, viewport, nearFar);\n            }\n        });\n    }\n\n    if (!opt.vrMode && opt.frameBuffer != Options::FrameBuffer::Default)\n    {\n        desktopProgram = std::make_shared<Program>();\n        if (!desktopProgram->LoadVertFrag(\"shader/desktop_vert.glsl\", \"shader/desktop_frag.glsl\"))\n        {\n            Log::E(\"Error loading desktop shader!\\n\");\n            return 1;\n        }\n    }\n\n#ifdef USE_SDL\n    inputBuddy = std::make_shared<InputBuddy>();\n\n    inputBuddy->OnQuit([this]()\n    {\n        // forward this back to main\n        quitCallback();\n    });\n\n    inputBuddy->OnResize([this](int newWidth, int newHeight)\n    {\n        glViewport(0, 0, newWidth, newHeight);\n        resizeCallback(newWidth, newHeight);\n    });\n\n    inputBuddy->OnKey(SDLK_ESCAPE, [this](bool down, uint16_t mod)\n    {\n        quitCallback();\n    });\n\n    inputBuddy->OnKey(SDLK_c, [this](bool down, uint16_t mod)\n    {\n        if (down)\n        {\n            opt.drawPointCloud = !opt.drawPointCloud;\n        }\n    });\n\n    inputBuddy->OnKey(SDLK_n, [this](bool down, uint16_t mod)\n    {\n        if (down && camerasConfig)\n        {\n            cameraIndex++;\n            if (cameraIndex >= (int)camerasConfig->GetNumCameras())\n            {\n                cameraIndex -= (int)camerasConfig->GetNumCameras();\n            }\n            flyCam->SetCameraMat(camerasConfig->GetCameraVec()[cameraIndex].mat);\n        }\n    });\n\n    inputBuddy->OnKey(SDLK_p, [this](bool down, uint16_t mod)\n    {\n        if (down && camerasConfig)\n        {\n            cameraIndex--;\n            if (cameraIndex < 0)\n            {\n                cameraIndex += (int)camerasConfig->GetNumCameras();\n            }\n            flyCam->SetCameraMat(camerasConfig->GetCameraVec()[cameraIndex].mat);\n        }\n    });\n\n    inputBuddy->OnKey(SDLK_f, [this](bool down, uint16_t mod)\n    {\n        if (down)\n        {\n            opt.drawCarpet = !opt.drawCarpet;\n        }\n    });\n\n    inputBuddy->OnKey(SDLK_y, [this](bool down, uint16_t mod)\n    {\n        if (down)\n        {\n            opt.drawCameraFrustums = !opt.drawCameraFrustums;\n        }\n    });\n\n    inputBuddy->OnKey(SDLK_h, [this](bool down, uint16_t mod)\n    {\n        if (down)\n        {\n            opt.drawCameraPath = !opt.drawCameraPath;\n        }\n    });\n\n    inputBuddy->OnKey(SDLK_RETURN, [this, vrConfigFilename](bool down, uint16_t mod)\n    {\n        if (down)\n        {\n            if (!vrConfig)\n            {\n                vrConfig = std::make_shared<VrConfig>();\n            }\n\n            if (opt.vrMode)\n            {\n                vrConfig->SetFloorMat(magicCarpet->GetCarpetMat());\n            }\n            else\n            {\n                glm::mat4 headMat = flyCam->GetCameraMat();\n                glm::vec3 pos = headMat[3];\n                pos -= glm::mat3(headMat) * glm::vec3(0.0f, 1.5f, 0.0f);\n                glm::mat4 floorMat = headMat;\n                floorMat[3] = glm::vec4(pos, 1.0f);\n                vrConfig->SetFloorMat(floorMat);\n            }\n\n            if (vrConfig->ExportJson(vrConfigFilename))\n            {\n                Log::I(\"Wrote \\\"%s\\\"\\n\", vrConfigFilename.c_str());\n            }\n            else\n            {\n                Log::E(\"Writing \\\"%s\\\" failed\\n\", vrConfigFilename.c_str());\n            }\n        }\n    });\n\n    inputBuddy->OnKey(SDLK_F1, [this](bool down, uint16_t mod)\n    {\n        if (down)\n        {\n            opt.drawFps = !opt.drawFps;\n        }\n    });\n\n    inputBuddy->OnKey(SDLK_a, [this](bool down, uint16_t mod)\n    {\n        virtualLeftStick.x += down ? -1.0f : 1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_d, [this](bool down, uint16_t mod)\n    {\n        virtualLeftStick.x += down ? 1.0f : -1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_w, [this](bool down, uint16_t mod)\n    {\n        virtualLeftStick.y += down ? 1.0f : -1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_s, [this](bool down, uint16_t mod)\n    {\n        virtualLeftStick.y += down ? -1.0f : 1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_LEFT, [this](bool down, uint16_t mod)\n    {\n        virtualRightStick.x += down ? -1.0f : 1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_RIGHT, [this](bool down, uint16_t mod)\n    {\n        virtualRightStick.x += down ? 1.0f : -1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_UP, [this](bool down, uint16_t mod)\n    {\n        virtualRightStick.y += down ? 1.0f : -1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_DOWN, [this](bool down, uint16_t mod)\n    {\n        virtualRightStick.y += down ? -1.0f : 1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_q, [this](bool down, uint16_t mod)\n    {\n        virtualRoll += down ? -1.0f : 1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_e, [this](bool down, uint16_t mod)\n    {\n        virtualRoll += down ? 1.0f : -1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_t, [this](bool down, uint16_t mod)\n    {\n        virtualUp += down ? 1.0f : -1.0f;\n    });\n\n    inputBuddy->OnKey(SDLK_g, [this](bool down, uint16_t mod)\n    {\n        virtualUp += down ? -1.0f : 1.0f;\n    });\n\n    inputBuddy->OnMouseButton([this](uint8_t button, bool down, glm::ivec2 pos)\n    {\n        if (button == 3) // right button\n        {\n            if (mouseLook != down)\n            {\n                inputBuddy->SetRelativeMouseMode(down);\n            }\n            mouseLook = down;\n        }\n    });\n\n    inputBuddy->OnMouseMotion([this](glm::ivec2 pos, glm::ivec2 rel)\n    {\n        if (mouseLook)\n        {\n            const float MOUSE_SENSITIVITY = 0.001f;\n            mouseLookStick.x += rel.x * MOUSE_SENSITIVITY;\n            mouseLookStick.y -= rel.y * MOUSE_SENSITIVITY;\n        }\n    });\n#endif // USE_SDL\n\n    fpsText = textRenderer->AddScreenTextWithDropShadow(glm::ivec2(0, 0), (int)TEXT_NUM_ROWS, WHITE, BLACK, \"fps:\");\n\n    return true;\n}\n\nvoid App::ProcessEvent(const SDL_Event& event)\n{\n#ifdef USE_SDL\n    inputBuddy->ProcessEvent(event);\n#endif\n}\n\nvoid App::UpdateFps(float fps)\n{\n    std::string text = \"fps: \" + std::to_string((int)fps);\n    textRenderer->RemoveText(fpsText);\n    fpsText = textRenderer->AddScreenTextWithDropShadow(glm::ivec2(0, 0), TEXT_NUM_ROWS, WHITE, BLACK, text);\n\n//#define FIND_BEST_NUM_BLOCKS_PER_WORKGROUP\n#ifdef FIND_BEST_NUM_BLOCKS_PER_WORKGROUP\n    Log::E(\"%s\\n\", text.c_str());\n    fpsVec.push_back(fps);\n    const uint32_t STEP_SIZE = 64;\n    if (fpsVec.size() == 2)\n    {\n        float dFps = fpsVec[1] - fpsVec[0];\n\n        uint32_t x = splatRenderer->numBlocksPerWorkgroup - STEP_SIZE;\n        Log::E(\"    (%.3f -> %.3f) dFps = %.3f, x = %u\\n\", fpsVec[0], fpsVec[1], dFps, x);\n        if (dFps < 0)\n        {\n            Log::E(\"    FPS DOWN, new x = %u\\n\", x - STEP_SIZE);\n            if (splatRenderer->numBlocksPerWorkgroup > STEP_SIZE)\n            {\n                splatRenderer->numBlocksPerWorkgroup = x - STEP_SIZE;\n            }\n        }\n        else\n        {\n            Log::E(\"    FPS UP, new x = %u\\n\", x + STEP_SIZE);\n            splatRenderer->numBlocksPerWorkgroup = x + STEP_SIZE;\n        }\n\n        fpsVec.clear();\n    }\n    else\n    {\n        splatRenderer->numBlocksPerWorkgroup = splatRenderer->numBlocksPerWorkgroup + STEP_SIZE;\n    }\n#endif\n}\n\nbool App::Process(float dt)\n{\n    if (opt.vrMode)\n    {\n        if (!xrBuddy->PollEvents())\n        {\n            Log::E(\"xrBuddy PollEvents failed\\n\");\n            return false;\n        }\n\n        if (!xrBuddy->SyncInput())\n        {\n            Log::E(\"xrBuddy SyncInput failed\\n\");\n            return false;\n        }\n\n        // copy vr input into MagicCarpet\n        MagicCarpet::Pose headPose, rightPose, leftPose;\n        if (!xrBuddy->GetActionPosition(\"head_pose\", &headPose.pos, &headPose.posValid, &headPose.posTracked))\n        {\n            Log::W(\"xrBuddy GetActionPosition(head_pose) failed\\n\");\n        }\n        if (!xrBuddy->GetActionOrientation(\"head_pose\", &headPose.rot, &headPose.rotValid, &headPose.rotTracked))\n        {\n            Log::W(\"xrBuddy GetActionOrientation(head_pose) failed\\n\");\n        }\n        xrBuddy->GetActionPosition(\"l_aim_pose\", &leftPose.pos, &leftPose.posValid, &leftPose.posTracked);\n        xrBuddy->GetActionOrientation(\"l_aim_pose\", &leftPose.rot, &leftPose.rotValid, &leftPose.rotTracked);\n        xrBuddy->GetActionPosition(\"r_aim_pose\", &rightPose.pos, &rightPose.posValid, &rightPose.posTracked);\n        xrBuddy->GetActionOrientation(\"r_aim_pose\", &rightPose.rot, &rightPose.rotValid, &rightPose.rotTracked);\n\n        glm::vec2 leftStick(0.0f, 0.0f);\n        glm::vec2 rightStick(0.0f, 0.0f);\n        bool valid = false;\n        bool changed = false;\n        xrBuddy->GetActionVec2(\"l_stick\", &leftStick, &valid, &changed);\n        xrBuddy->GetActionVec2(\"r_stick\", &rightStick, &valid, &changed);\n\n        // Convert trackpad into a \"stick\", for HTC Vive controllers\n        glm::vec2 leftTrackpadStick(0.0f, 0.0f);\n        bool leftTrackpadClick = false;\n        xrBuddy->GetActionBool(\"l_trackpad_click\", &leftTrackpadClick, &valid, &changed);\n        if (leftTrackpadClick && valid)\n        {\n            xrBuddy->GetActionFloat(\"l_trackpad_x\", &leftTrackpadStick.x, &valid, &changed);\n            xrBuddy->GetActionFloat(\"l_trackpad_y\", &leftTrackpadStick.y, &valid, &changed);\n        }\n        else\n        {\n            leftTrackpadStick = glm::vec2(0.0f, 0.0f);\n        }\n\n        glm::vec2 rightTrackpadStick(0.0f, 0.0f);\n        bool rightTrackpadClick = false;\n        xrBuddy->GetActionBool(\"r_trackpad_click\", &rightTrackpadClick, &valid, &changed);\n        if (rightTrackpadClick && valid)\n        {\n            xrBuddy->GetActionFloat(\"r_trackpad_x\", &rightTrackpadStick.x, &valid, &changed);\n            xrBuddy->GetActionFloat(\"r_trackpad_y\", &rightTrackpadStick.y, &valid, &changed);\n        }\n        else\n        {\n            rightTrackpadStick = glm::vec2(0.0f, 0.0f);\n        }\n\n        MagicCarpet::ButtonState buttonState;\n        xrBuddy->GetActionBool(\"l_select_click\", &buttonState.leftTrigger, &valid, &changed);\n        xrBuddy->GetActionBool(\"r_select_click\", &buttonState.rightTrigger, &valid, &changed);\n        xrBuddy->GetActionBool(\"l_squeeze_click\", &buttonState.leftGrip, &valid, &changed);\n        xrBuddy->GetActionBool(\"r_squeeze_click\", &buttonState.rightGrip, &valid, &changed);\n        magicCarpet->Process(headPose, leftPose, rightPose, leftStick + leftTrackpadStick,\n                             rightStick + rightTrackpadStick, buttonState, dt);\n    }\n#ifdef USE_SDL\n\n    InputBuddy::Joypad joypad = inputBuddy->GetJoypad();\n    float roll = 0.0f;\n    roll -= joypad.lb ? 1.0f : 0.0f;\n    roll += joypad.rb ? 1.0f : 0.0f;\n    flyCam->Process(glm::clamp(joypad.leftStick + virtualLeftStick, -1.0f, 1.0f),\n                    glm::clamp(joypad.rightStick + virtualRightStick, -1.0f, 1.0f) + mouseLookStick / (dt > 0.0f ? dt : 1.0f),\n                    glm::clamp(roll + virtualRoll, -1.0f, 1.0f), glm::clamp(virtualUp, -1.0f, 1.0f), dt);\n    mouseLookStick = glm::vec2(0.0f, 0.0f);\n\n#endif\n    return true;\n}\n\nbool App::Render(float dt, const glm::ivec2& windowSize)\n{\n    int width = windowSize.x;\n    int height = windowSize.y;\n\n    if (opt.vrMode)\n    {\n        if (xrBuddy->SessionReady())\n        {\n            if (!xrBuddy->RenderFrame())\n            {\n                Log::E(\"xrBuddy RenderFrame failed\\n\");\n                return false;\n            }\n        }\n        else\n        {\n            std::this_thread::sleep_for(std::chrono::milliseconds(10));\n        }\n#ifndef __ANDROID__\n        // render desktop.\n        Clear(windowSize, true);\n        RenderDesktop(windowSize, desktopProgram, xrBuddy->GetColorTexture(), true);\n\n        if (opt.drawFps)\n        {\n            glm::vec4 viewport(0.0f, 0.0f, (float)width, (float)height);\n            glm::vec2 nearFar(Z_NEAR, Z_FAR);\n            glm::mat4 projMat = glm::perspective(FOVY, (float)width / (float)height, Z_NEAR, Z_FAR);\n            textRenderer->Render(glm::mat4(1.0f), projMat, viewport, nearFar);\n        }\n#endif\n    }\n    else\n    {\n        // lazy init of fbo, fbo is only used for HalfFloat, Float option.\n        if (opt.frameBuffer != Options::FrameBuffer::Default && fboSize != windowSize)\n        {\n            fbo = std::make_shared<FrameBuffer>();\n\n            Texture::Params texParams;\n            texParams.minFilter = FilterType::Nearest;\n            texParams.magFilter = FilterType::Nearest;\n            texParams.sWrap = WrapType::ClampToEdge;\n            texParams.tWrap = WrapType::ClampToEdge;\n            if (opt.frameBuffer == Options::FrameBuffer::HalfFloat)\n            {\n                fboColorTex = std::make_shared<Texture>(windowSize.x, windowSize.y,\n                                                        GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT,\n                                                        texParams);\n            }\n            else if (opt.frameBuffer == Options::FrameBuffer::Float)\n            {\n                fboColorTex = std::make_shared<Texture>(windowSize.x, windowSize.y,\n                                                        GL_RGBA32F, GL_RGBA, GL_FLOAT,\n                                                        texParams);\n            }\n            else\n            {\n                Log::E(\"BAD opt.frameBuffer type!\\n\");\n            }\n\n            fbo->AttachColor(fboColorTex);\n\n            fboSize = windowSize;\n        }\n\n        if (opt.frameBuffer != Options::FrameBuffer::Default && fbo)\n        {\n            fbo->Bind();\n        }\n\n        Clear(windowSize, true);\n\n        glm::mat4 cameraMat = flyCam->GetCameraMat();\n        glm::vec4 viewport(0.0f, 0.0f, (float)width, (float)height);\n        glm::vec2 nearFar(Z_NEAR, Z_FAR);\n        glm::mat4 projMat = glm::perspective(FOVY, (float)width / (float)height, Z_NEAR, Z_FAR);\n\n        if (opt.drawDebug)\n        {\n            debugRenderer->Render(cameraMat, projMat, viewport, nearFar);\n        }\n\n        if (cameraPathRenderer)\n        {\n            cameraPathRenderer->SetShowCameras(opt.drawCameraFrustums);\n            cameraPathRenderer->SetShowPath(opt.drawCameraPath);\n            cameraPathRenderer->Render(cameraMat, projMat, viewport, nearFar);\n        }\n\n        if (opt.drawCarpet)\n        {\n            magicCarpet->Render(cameraMat, projMat, viewport, nearFar);\n        }\n\n        if (opt.drawPointCloud && pointRenderer)\n        {\n            pointRenderer->Render(cameraMat, projMat, viewport, nearFar);\n        }\n        else\n        {\n            splatRenderer->Sort(cameraMat, projMat, viewport, nearFar);\n            splatRenderer->Render(cameraMat, projMat, viewport, nearFar);\n        }\n\n        if (opt.drawFps)\n        {\n            textRenderer->Render(cameraMat, projMat, viewport, nearFar);\n        }\n\n        if (opt.frameBuffer != Options::FrameBuffer::Default && fbo)\n        {\n            // render fbo colorTexture as a full screen quad to the default fbo\n            glBindFramebuffer(GL_FRAMEBUFFER, 0);\n            Clear(windowSize, true);\n            RenderDesktop(windowSize, desktopProgram, fbo->GetColorTexture()->texture, false);\n        }\n    }\n\n    debugRenderer->EndFrame();\n\n    frameNum++;\n\n    return true;\n}\n\nvoid App::OnQuit(const VoidCallback& cb)\n{\n    quitCallback = cb;\n}\n\nvoid App::OnResize(const ResizeCallback& cb)\n{\n    resizeCallback = cb;\n}\n"
  },
  {
    "path": "src/app.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <functional>\n#include <glm/glm.hpp>\n#include <memory>\n#include <string>\n\n#include \"maincontext.h\"\n\nclass CamerasConfig;\nclass CameraPathRenderer;\nclass DebugRenderer;\nclass FlyCam;\nstruct FrameBuffer;\nclass GaussianCloud;\nclass InputBuddy;\nclass MagicCarpet;\nclass PointCloud;\nclass PointRenderer;\nclass Program;\nclass SplatRenderer;\nclass TextRenderer;\nstruct Texture;\nclass VrConfig;\nclass XrBuddy;\n\nunion SDL_Event;\n\nclass App\n{\npublic:\n    App(MainContext& mainContextIn);\n\n    enum ParseResult\n    {\n        SUCCESS_RESULT,\n        ERROR_RESULT,\n        QUIT_RESULT\n    };\n\n    ParseResult ParseArguments(int argc, const char* argv[]);\n    bool Init();\n    bool IsFullscreen() const { return opt.fullscreen; }\n    void UpdateFps(float fps);\n    void ProcessEvent(const SDL_Event& event);\n    bool Process(float dt);\n    bool Render(float dt, const glm::ivec2& windowSize);\n\n    using VoidCallback = std::function<void()>;\n    void OnQuit(const VoidCallback& cb);\n\n    using ResizeCallback = std::function<void(int, int)>;\n    void OnResize(const ResizeCallback& cb);\n\n    struct Options\n    {\n        enum class FrameBuffer\n        {\n            Default,\n            HalfFloat,\n            Float\n        };\n        bool vrMode = false;\n        bool fullscreen = false;\n        FrameBuffer frameBuffer = FrameBuffer::Default;\n        bool drawCarpet = false;\n        bool drawPointCloud = false;\n        bool drawDebug = true;\n        bool debugLogging = false;\n        bool drawFps = true;\n        bool drawCameraFrustums = false;\n        bool drawCameraPath = false;\n        bool importFullSH = true;\n    };\n\nprotected:\n    MainContext& mainContext;\n    Options opt;\n    std::string plyFilename;\n    std::shared_ptr<DebugRenderer> debugRenderer;\n    std::shared_ptr<CameraPathRenderer> cameraPathRenderer;\n    std::shared_ptr<TextRenderer> textRenderer;\n    std::shared_ptr<XrBuddy> xrBuddy;\n\n    std::shared_ptr<CamerasConfig> camerasConfig;\n    std::shared_ptr<VrConfig> vrConfig;\n\n    int cameraIndex;\n    std::shared_ptr<FlyCam> flyCam;\n    std::shared_ptr<MagicCarpet> magicCarpet;\n\n    std::shared_ptr<PointCloud> pointCloud;\n    std::shared_ptr<GaussianCloud> gaussianCloud;\n    std::shared_ptr<PointRenderer> pointRenderer;\n    std::shared_ptr<SplatRenderer> splatRenderer;\n\n    std::shared_ptr<Program> desktopProgram;\n    std::shared_ptr<FrameBuffer> fbo;\n    glm::ivec2 fboSize = {0, 0};\n    std::shared_ptr<Texture> fboColorTex;\n\n    std::shared_ptr<InputBuddy> inputBuddy;\n\n    glm::vec2 virtualLeftStick;\n    glm::vec2 virtualRightStick;\n    glm::vec2 mouseLookStick;\n    bool mouseLook;\n    float virtualRoll;\n    float virtualUp;\n    uint32_t fpsText;\n    uint32_t frameNum;\n\n    VoidCallback quitCallback;\n    ResizeCallback resizeCallback;\n\n    std::vector<float> fpsVec;\n};\n"
  },
  {
    "path": "src/camerapathrenderer.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"camerapathrenderer.h\"\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#else\n#include <GL/glew.h>\n#endif\n\n#include \"core/log.h\"\n#include \"core/util.h\"\n\n#include \"camerasconfig.h\"\n\nCameraPathRenderer::CameraPathRenderer()\n{\n}\n\nCameraPathRenderer::~CameraPathRenderer()\n{\n}\n\nbool CameraPathRenderer::Init(const std::vector<Camera>& cameraVec)\n{\n\tddProg = std::make_shared<Program>();\n    if (!ddProg->LoadVertFrag(\"shader/debugdraw_vert.glsl\", \"shader/debugdraw_frag.glsl\"))\n    {\n        Log::E(\"Error loading CameraPathRenderer shader!\\n\");\n        return false;\n    }\n\n\tBuildCamerasVao(cameraVec);\n\tBuildPathVao(cameraVec);\n\n\treturn true;\n}\n\n// viewport = (x, y, width, height)\nvoid CameraPathRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n\t\t\t\t\t\t\t\tconst glm::vec4& viewport, const glm::vec2& nearFar)\n{\n\tif (!showCameras && !showPath)\n\t{\n\t\treturn;\n\t}\n\n\tGL_ERROR_CHECK(\"CameraPathRenderer::Render() begin\");\n\n\tglm::mat4 modelViewProjMat = projMat * glm::inverse(cameraMat);\n\n\tddProg->Bind();\n\tddProg->SetUniform(\"modelViewProjMat\", modelViewProjMat);\n\n\tif (showCameras)\n\t{\n\t\tcamerasVao->Bind();\n\t\tglDrawElements(GL_LINES, numCameraVerts, GL_UNSIGNED_INT, nullptr);\n\t\tcamerasVao->Unbind();\n\t}\n\n\tif (showPath)\n\t{\n\t\tpathVao->Bind();\n\t\tglDrawElements(GL_LINES, numPathVerts, GL_UNSIGNED_INT, nullptr);\n\t\tpathVao->Unbind();\n\t}\n\n\tGL_ERROR_CHECK(\"CameraPathRenderer::Render() draw\");\n}\n\nvoid CameraPathRenderer::BuildCamerasVao(const std::vector<Camera>& cameraVec)\n{\n\tcamerasVao = std::make_shared<VertexArrayObject>();\n\n\tconst uint32_t NUM_LINES = 8;\n\tconst float FRUSTUM_LEN = 0.1f;\n\tconst glm::vec4 FRUSTUM_COLOR(0.0f, 1.0f, 0.0f, 1.0f);\n\n\tnumCameraVerts = (uint32_t)cameraVec.size() * NUM_LINES * 2;\n\n\tstd::vector<glm::vec3> posVec;\n\tposVec.reserve(numCameraVerts);\n\tstd::vector<glm::vec4> colVec;\n\tcolVec.reserve(numCameraVerts);\n\tstd::vector<uint32_t> indexVec;\n\tindexVec.reserve(numCameraVerts);\n\n\t// build lines for each frustum\n\tfor (auto& c : cameraVec)\n\t{\n\t\tfloat xRadius = FRUSTUM_LEN / cosf(c.fov.x / 2.0f);\n\t\tfloat xOffset = xRadius * sinf(c.fov.x / 2.0f);\n\t\tfloat yRadius = FRUSTUM_LEN / cosf(c.fov.y / 2.0f);\n\t\tfloat yOffset = yRadius * sinf(c.fov.y / 2.0f);\n\n\t\tconst uint32_t NUM_FRUSTUM_VERTS = 5;\n\t\tglm::vec3 verts[NUM_FRUSTUM_VERTS] = {\n\t\t    glm::vec3(0.0f, 0.0f, 0.0f),\n\t\t\tglm::vec3(xOffset, yOffset, -FRUSTUM_LEN),\n\t\t\tglm::vec3(-xOffset, yOffset, -FRUSTUM_LEN),\n\t\t\tglm::vec3(-xOffset, -yOffset, -FRUSTUM_LEN),\n\t\t\tglm::vec3(xOffset, -yOffset, -FRUSTUM_LEN)\n\t\t};\n\t\tfor (int i = 0; i < NUM_FRUSTUM_VERTS; i++)\n\t\t{\n\t\t\tverts[i] = XformPoint(c.mat, verts[i]);\n\t\t}\n\n\t\tposVec.push_back(verts[0]); posVec.push_back(verts[1]);\n\t\tposVec.push_back(verts[0]); posVec.push_back(verts[2]);\n\t\tposVec.push_back(verts[0]); posVec.push_back(verts[3]);\n\t\tposVec.push_back(verts[0]); posVec.push_back(verts[4]);\n\n\t\tposVec.push_back(verts[1]); posVec.push_back(verts[2]);\n\t\tposVec.push_back(verts[2]); posVec.push_back(verts[3]);\n\t\tposVec.push_back(verts[3]); posVec.push_back(verts[4]);\n\t\tposVec.push_back(verts[4]); posVec.push_back(verts[1]);\n\n\t\tfor (int i = 0; i < NUM_LINES; i++)\n\t\t{\n\t\t\tcolVec.push_back(FRUSTUM_COLOR); colVec.push_back(FRUSTUM_COLOR);\n\t\t}\n\t}\n\n    auto posBuffer = std::make_shared<BufferObject>(GL_ARRAY_BUFFER, posVec);\n    auto colBuffer = std::make_shared<BufferObject>(GL_ARRAY_BUFFER, colVec);\n\n\t// build element array\n    assert(numCameraVerts <= std::numeric_limits<uint32_t>::max());\n    for (uint32_t i = 0; i < numCameraVerts; i++)\n    {\n        indexVec.push_back(i);\n    }\n    auto indexBuffer = std::make_shared<BufferObject>(GL_ELEMENT_ARRAY_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT);\n\n\t// setup vertex array object with buffers\n    camerasVao->SetAttribBuffer(ddProg->GetAttribLoc(\"position\"), posBuffer);\n    camerasVao->SetAttribBuffer(ddProg->GetAttribLoc(\"color\"), colBuffer);\n    camerasVao->SetElementBuffer(indexBuffer);\n}\n\nvoid CameraPathRenderer::BuildPathVao(const std::vector<Camera>& cameraVec)\n{\n\tpathVao = std::make_shared<VertexArrayObject>();\n\n\tconst uint32_t NUM_LINES = 1;\n\tconst glm::vec4 PATH_COLOR(0.0f, 1.0f, 1.0f, 1.0f);\n\n\tnumPathVerts = (uint32_t)cameraVec.size() * NUM_LINES * 2;\n\n\tstd::vector<glm::vec3> posVec;\n\tposVec.reserve(numPathVerts);\n\tstd::vector<glm::vec4> colVec;\n\tcolVec.reserve(numPathVerts);\n\tstd::vector<uint32_t> indexVec;\n\tindexVec.reserve(numPathVerts);\n\n\t// build lines for each path segment\n\tif (cameraVec.size() > 1)\n\t{\n\t\tconst Camera* prev = &cameraVec[0];\n\t\tfor (size_t i = 1; i < cameraVec.size(); i++)\n\t\t{\n\t\t\tconst Camera* curr = cameraVec.data() + i;\n\t\t\tglm::vec3 prevPos = glm::vec3(prev->mat[3]);\n\t\t\tglm::vec3 currPos = glm::vec3(curr->mat[3]);\n\t\t\tposVec.push_back(prevPos);\n\t\t\tposVec.push_back(currPos);\n\t\t\tcolVec.push_back(PATH_COLOR);\n\t\t\tcolVec.push_back(PATH_COLOR);\n\t\t\tprev = curr;\n\t\t}\n\t}\n\telse\n\t{\n\t\tposVec.push_back(glm::vec3(0.0f, 0.0f, 0.0f));\n\t\tposVec.push_back(glm::vec3(0.0f, 0.0f, 0.0f));\n\t\tcolVec.push_back(PATH_COLOR);\n\t\tcolVec.push_back(PATH_COLOR);\n\t}\n\n    auto posBuffer = std::make_shared<BufferObject>(GL_ARRAY_BUFFER, posVec);\n    auto colBuffer = std::make_shared<BufferObject>(GL_ARRAY_BUFFER, colVec);\n\n\t// build element array\n    assert(numPathVerts <= std::numeric_limits<uint32_t>::max());\n    for (uint32_t i = 0; i < numPathVerts; i++)\n    {\n        indexVec.push_back(i);\n    }\n    auto indexBuffer = std::make_shared<BufferObject>(GL_ELEMENT_ARRAY_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT);\n\n\t// setup vertex array object with buffers\n    pathVao->SetAttribBuffer(ddProg->GetAttribLoc(\"position\"), posBuffer);\n    pathVao->SetAttribBuffer(ddProg->GetAttribLoc(\"color\"), colBuffer);\n    pathVao->SetElementBuffer(indexBuffer);\n}\n"
  },
  {
    "path": "src/camerapathrenderer.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <memory>\n#include <stdint.h>\n#include <vector>\n\n#include \"core/program.h\"\n#include \"core/vertexbuffer.h\"\n\nstruct Camera;\n\nclass CameraPathRenderer\n{\npublic:\n    CameraPathRenderer();\n    ~CameraPathRenderer();\n\n    bool Init(const std::vector<Camera>& cameraVec);\n\n    void SetShowCameras(bool showCamerasIn) { showCameras = showCamerasIn; }\n    void SetShowPath(bool showPathIn) { showPath = showPathIn; }\n\n    // viewport = (x, y, width, height)\n    void Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                const glm::vec4& viewport, const glm::vec2& nearFar);\n\nprotected:\n    void BuildCamerasVao(const std::vector<Camera>& cameraVec);\n    void BuildPathVao(const std::vector<Camera>& cameraVec);\n\n    std::shared_ptr<Program> ddProg;\n    std::shared_ptr<VertexArrayObject> camerasVao;\n    uint32_t numCameraVerts;\n    std::shared_ptr<VertexArrayObject> pathVao;\n    uint32_t numPathVerts;\n\n    bool showCameras = true;\n    bool showPath = true;\n};\n"
  },
  {
    "path": "src/camerasconfig.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"camerasconfig.h\"\n\n#include <fstream>\n#include <nlohmann/json.hpp>\n#include <iostream>\n\n#include \"core/log.h\"\n#include \"core/util.h\"\n\nCamerasConfig::CamerasConfig()\n{\n\n}\n\nbool CamerasConfig::ImportJson(const std::string& jsonFilename)\n{\n    std::ifstream f(jsonFilename);\n    if (f.fail())\n    {\n        return false;\n    }\n\n    try\n    {\n        nlohmann::json data = nlohmann::json::parse(f);\n        for (auto&& o : data)\n        {\n            int id = o[\"id\"].template get<int>();\n\n            nlohmann::json jPos = o[\"position\"];\n            glm::vec3 pos(jPos[0].template get<float>(), jPos[1].template get<float>(), jPos[2].template get<float>());\n\n            nlohmann::json jRot = o[\"rotation\"];\n            glm::mat3 rot(jRot[0][0].template get<float>(), jRot[1][0].template get<float>(), jRot[2][0].template get<float>(),\n                          jRot[0][1].template get<float>(), jRot[1][1].template get<float>(), jRot[2][1].template get<float>(),\n                          jRot[0][2].template get<float>(), jRot[1][2].template get<float>(), jRot[2][2].template get<float>());\n            int width = o[\"width\"].template get<int>();\n            int height = o[\"height\"].template get<int>();\n            float fx = o[\"fx\"].template get<float>();\n            float fy = o[\"fy\"].template get<float>();\n\n            glm::vec2 fov(2.0f * atanf(width / (2.0f * fx)),\n                          2.0f * atanf(height / (2.0f * fx)));\n\n            glm::mat4 mat(glm::vec4(rot[0], 0.0f),\n                          glm::vec4(-rot[1], 0.0f),\n                          glm::vec4(-rot[2], 0.0f),\n                          glm::vec4(pos, 1.0f));\n\n            // swizzle rot to make -z forward and y up.\n            cameraVec.emplace_back(Camera{mat, fov});\n        }\n    }\n    catch (const nlohmann::json::exception& e)\n    {\n        std::string s = e.what();\n        Log::E(\"CamerasConfig::ImportJson exception: %s\\n\", s.c_str());\n        return false;\n    }\n\n    return true;\n}\n\nvoid CamerasConfig::EstimateFloorPlane(glm::vec3& normalOut, glm::vec3& posOut) const\n{\n    if (cameraVec.empty())\n    {\n        normalOut = glm::vec3(0.0f, 1.0f, 0.0f);\n        posOut = glm::vec3(0.0f, 0.0f, 0.0f);\n    }\n\n    glm::vec3 avgUp(0.0f, 0.0f, 0.0f);\n    float weight = 1.0f / (float)cameraVec.size();\n    for (auto&& c : cameraVec)\n    {\n        glm::vec3 up(c.mat[1]);\n        avgUp += weight * up;\n    }\n    avgUp = SafeNormalize(avgUp, glm::vec3(0.0f, 1.0f, 0.0f));\n\n    float avgDist = 0.0f;\n    for (auto&& c : cameraVec)\n    {\n        glm::vec3 pos(c.mat[3]);\n        avgDist += weight * glm::dot(pos, avgUp);\n    }\n\n    normalOut = avgUp;\n    posOut = avgUp * avgDist;\n}\n"
  },
  {
    "path": "src/camerasconfig.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <string>\n#include <vector>\n\nstruct Camera\n{\n\tglm::mat4 mat;  // inverse view matrix\n\tglm::vec2 fov;\n};\n\nclass CamerasConfig\n{\npublic:\n    CamerasConfig();\n\n    bool ImportJson(const std::string& jsonFilename);\n\n    const std::vector<Camera>& GetCameraVec() const { return cameraVec; }\n\tsize_t GetNumCameras() const { return cameraVec.size(); }\n\n\tvoid EstimateFloorPlane(glm::vec3& normalOut, glm::vec3& posOut) const;\nprotected:\n\n    std::vector<Camera> cameraVec;\n};\n"
  },
  {
    "path": "src/core/binaryattribute.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"binaryattribute.h\"\n\nstatic uint32_t propertyTypeSizeArr[(size_t)BinaryAttribute::Type::NumTypes] = {\n    0, // Unknown\n    sizeof(int8_t), // Char\n    sizeof(uint8_t), // UChar\n    sizeof(int16_t), // Short\n    sizeof(uint16_t), // UShort\n    sizeof(int32_t), // Int\n    sizeof(uint32_t), // UInt\n    sizeof(float), // Float\n    sizeof(double) // Double\n};\n\nBinaryAttribute::BinaryAttribute(Type typeIn, size_t offsetIn) :\n    type(typeIn),\n    size(propertyTypeSizeArr[(uint32_t)typeIn]),\n    offset(offsetIn)\n{\n    ;\n}\n"
  },
  {
    "path": "src/core/binaryattribute.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <cassert>\n#include <cstdint>\n#include <functional>\n\nclass BinaryAttribute\n{\npublic:\n    enum class Type\n    {\n        Unknown,\n        Char,\n        UChar,\n        Short,\n        UShort,\n        Int,\n        UInt,\n        Float,\n        Double,\n        NumTypes\n    };\n\n    BinaryAttribute() : type(Type::Unknown), size(0), offset(0) {}\n    BinaryAttribute(Type typeIn, size_t offsetIn);\n\n    template <typename T>\n    const T* Get(const void* data) const\n    {\n        if (type == Type::Unknown)\n        {\n            return nullptr;\n        }\n        else\n        {\n            assert(size == sizeof(T));\n            return reinterpret_cast<const T*>(static_cast<const uint8_t*>(data) + offset);\n        }\n    }\n\n    template <typename T>\n    T* Get(void* data)\n    {\n        if (type == Type::Unknown)\n        {\n            return nullptr;\n        }\n        else\n        {\n            assert(size == sizeof(T));\n            return reinterpret_cast<T*>(static_cast<uint8_t*>(data) + offset);\n        }\n    }\n\n    template <typename T>\n    const T Read(const void* data) const\n    {\n        const T* ptr = Get<T>(data);\n        return ptr ? *ptr : 0;\n    }\n\n    template <typename T>\n    bool Write(void* data, const T& val)\n    {\n        T* ptr = Get<T>(data);\n        if (ptr)\n        {\n            *ptr = val;\n            return true;\n        }\n        else\n        {\n            return false;\n        }\n    }\n\n    template<typename T>\n    void ForEachMut(void* data, size_t stride, size_t count, const std::function<void(T*)>& cb)\n    {\n        assert(type != Type::Unknown);\n        assert(data);\n        uint8_t* ptr = (uint8_t*)data;\n        for (size_t i = 0; i < count; i++)\n        {\n            cb((T*)(ptr + offset));\n            ptr += stride;\n        }\n    }\n\n    template<typename T>\n    void ForEach(const void* data, size_t stride, size_t count, const std::function<void(const T*)>& cb) const\n    {\n        assert(type != Type::Unknown);\n        assert(data);\n        const uint8_t* ptr = (const uint8_t*)data;\n        for (size_t i = 0; i < count; i++)\n        {\n            cb((const T*)(ptr + offset));\n            ptr += stride;\n        }\n    }\n\n    Type type;\n    size_t size;\n    size_t offset;\n};\n"
  },
  {
    "path": "src/core/debugrenderer.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"debugrenderer.h\"\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#else\n#include <GL/glew.h>\n#define GL_GLEXT_PROTOTYPES 1\n#include <SDL2/SDL_opengl.h>\n#include <SDL2/SDL_opengl_glext.h>\n#endif\n\n#include <memory>\n#include <vector>\n\n#include \"log.h\"\n#include \"util.h\"\n#include \"program.h\"\n\nDebugRenderer::DebugRenderer()\n{\n\n}\n\nbool DebugRenderer::Init()\n{\n    ddProg = std::make_shared<Program>();\n    if (!ddProg->LoadVertFrag(\"shader/debugdraw_vert.glsl\", \"shader/debugdraw_frag.glsl\"))\n    {\n        Log::E(\"Error loading DebugRenderer shader!\\n\");\n        return false;\n    }\n    return true;\n}\n\nvoid DebugRenderer::Line(const glm::vec3& start, const glm::vec3& end, const glm::vec3& color)\n{\n    linePositionVec.push_back(start);\n    linePositionVec.push_back(end);\n    lineColorVec.push_back(color);\n    lineColorVec.push_back(color);\n}\n\nvoid DebugRenderer::Transform(const glm::mat4& m, float axisLen)\n{\n    glm::vec3 x = glm::vec3(m[0]);\n    glm::vec3 y = glm::vec3(m[1]);\n    glm::vec3 z = glm::vec3(m[2]);\n    x = axisLen * glm::normalize(x);\n    y = axisLen * glm::normalize(y);\n    z = axisLen * glm::normalize(z);\n    glm::vec3 p = glm::vec3(m[3]);\n\n    Line(p, p + x, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f));\n    Line(p, p + y, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f));\n    Line(p, p + z, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f));\n}\n\nvoid DebugRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                           const glm::vec4& viewport, const glm::vec2& nearFar)\n{\n    ddProg->Bind();\n    glm::mat4 modelViewProjMat = projMat * glm::inverse(cameraMat);\n    ddProg->SetUniform(\"modelViewProjMat\", modelViewProjMat);\n    ddProg->SetAttrib(\"position\", linePositionVec.data());\n    ddProg->SetAttrib(\"color\", lineColorVec.data());\n    glDrawArrays(GL_LINES, 0, (GLsizei)linePositionVec.size());\n}\n\nvoid DebugRenderer::EndFrame()\n{\n    linePositionVec.clear();\n    lineColorVec.clear();\n}\n"
  },
  {
    "path": "src/core/debugrenderer.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <memory>\n#include <vector>\n\nclass Program;\n\nclass DebugRenderer\n{\npublic:\n\tDebugRenderer();\n\n\tbool Init();\n\n\t// viewport = (x, y, width, height)\n\tvoid Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                const glm::vec4& viewport, const glm::vec2& nearFar);\n\n\t// call at end of frame.\n\tvoid EndFrame();\n\n\tvoid Line(const glm::vec3& start, const glm::vec3& end, const glm::vec3& color);\n\tvoid Transform(const glm::mat4& m, float axisLen = 1.0f);\n\nprotected:\n\tstd::shared_ptr<Program> ddProg;\n\tstd::vector<glm::vec3> linePositionVec;\n\tstd::vector<glm::vec3> lineColorVec;\n};\n\n\n"
  },
  {
    "path": "src/core/framebuffer.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"framebuffer.h\"\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#else\n#include <GL/glew.h>\n#define GL_GLEXT_PROTOTYPES 1\n#include <SDL2/SDL_opengl.h>\n#include <SDL2/SDL_opengl_glext.h>\n#endif\n\n#include \"texture.h\"\n\nFrameBuffer::FrameBuffer()\n{\n    glGenFramebuffers(1, &fbo);\n}\n\nFrameBuffer::~FrameBuffer()\n{\n    glDeleteFramebuffers(1, &fbo);\n    fbo = 0;\n}\n\nvoid FrameBuffer::Bind() const\n{\n    glBindFramebuffer(GL_FRAMEBUFFER, fbo);\n}\n\nvoid FrameBuffer::AttachColor(std::shared_ptr<Texture> colorTex)\n{\n    Bind();\n    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex->texture, 0);\n    colorAttachment = colorTex;\n}\n\nvoid FrameBuffer::AttachDepth(std::shared_ptr<Texture> depthTex)\n{\n    Bind();\n    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTex->texture, 0);\n    depthAttachment = depthTex;\n}\n\nvoid FrameBuffer::AttachStencil(std::shared_ptr<Texture> stencilTex)\n{\n    Bind();\n    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, stencilTex->texture, 0);\n    stencilAttachment = stencilTex;\n}\n\nbool FrameBuffer::IsComplete() const\n{\n    return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;\n}\n"
  },
  {
    "path": "src/core/framebuffer.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <stdint.h>\n#include <memory>\n\nstruct Texture;\n\nstruct FrameBuffer\n{\n    FrameBuffer();\n    ~FrameBuffer();\n\n    void Bind() const;\n    void AttachColor(std::shared_ptr<Texture> colorTex);\n    void AttachDepth(std::shared_ptr<Texture> depthTex);\n    void AttachStencil(std::shared_ptr<Texture> stencilTex);\n\n    bool IsComplete() const;\n\n    std::shared_ptr<Texture> GetColorTexture() const { return colorAttachment; }\n    std::shared_ptr<Texture> GetDepthTexture() const { return depthAttachment; }\n    std::shared_ptr<Texture> GetStencilTexture() const { return stencilAttachment; }\n\n    uint32_t fbo;\n    std::shared_ptr<Texture> colorAttachment;\n    std::shared_ptr<Texture> depthAttachment;\n    std::shared_ptr<Texture> stencilAttachment;\n};\n"
  },
  {
    "path": "src/core/image.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"image.h\"\n//#include \"util.h\"\n#include \"log.h\"\n\n#include <string.h>\n\nextern \"C\" {\n#include <png.h>\n}\n\n#include \"util.h\"\n\nImage::Image() : width(0), height(0), pixelFormat(PixelFormat::R), isSRGB(false)\n{\n}\n\nbool Image::Load(const std::string& filenameIn)\n{\n    std::string fullFilename = GetRootPath() + filenameIn;\n    const char* filename = fullFilename.c_str();\n\n#ifdef _WIN32\n    FILE *fp = NULL;\n    fopen_s(&fp, filename, \"rb\");\n#else\n    FILE *fp = fopen(filename, \"rb\");\n#endif\n    if (!fp)\n    {\n        Log::E(\"Failed to load texture \\\"%s\\\"\\n\", filename);\n        return false;\n    }\n\n    unsigned char header[8];\n    fread(header, 1, 8, fp);\n    if (png_sig_cmp(header, 0, 8))\n    {\n        Log::E(\"Texture \\\"%s\\\" is not a valid PNG file\\n\", filename);\n        return false;\n    }\n\n    png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);\n    if (!png_ptr)\n    {\n        Log::E(\"png_create_read_struct() failed\\n\");\n        return false;\n    }\n\n    png_infop info_ptr = png_create_info_struct(png_ptr);\n    if (!info_ptr)\n    {\n        Log::E(\"png_create_info_struct() failed\\n\");\n        png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL);\n        return false;\n    }\n\n    png_init_io(png_ptr, fp);\n    png_set_sig_bytes(png_ptr, 8);\n    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);\n\n    png_byte** row_pointers = png_get_rows(png_ptr, info_ptr);\n\n    png_uint_32 w, h;\n    int bit_depth, color_type;\n    png_get_IHDR(png_ptr, info_ptr, &w, &h, &bit_depth, &color_type, NULL, NULL, NULL);\n\n    bool loaded = false;\n    if (bit_depth != 8)\n    {\n        Log::E(\"bad bit depth for texture \\\"%s\\n\", filename);\n    }\n    else\n    {\n        int pixelSize;\n        switch (color_type)\n        {\n        case PNG_COLOR_TYPE_GRAY:\n            pixelFormat = PixelFormat::R;\n            pixelSize = 1;\n            break;\n        case PNG_COLOR_TYPE_GA:\n            pixelFormat = PixelFormat::RA;\n            pixelSize = 2;\n            break;\n        case PNG_COLOR_TYPE_RGB:\n            pixelFormat = PixelFormat::RGB;\n            pixelSize = 3;\n            break;\n        case PNG_COLOR_TYPE_RGBA:\n            pixelFormat = PixelFormat::RGBA;\n            pixelSize = 4;\n            break;\n        default:\n            Log::E(\"unsupported pixel format %d for image \\\"%s\\n\", color_type, filename);\n            return false;\n        }\n\n        width = w;\n        height = h;\n        data.resize(width * height * pixelSize);\n\n        // copy row pointers into data vector\n        for (int i = 0; i < (int)height; ++i)\n        {\n            memcpy(&data[0] + i * width * pixelSize, row_pointers[height - 1 - i], width * pixelSize);\n        }\n\n        // pre-multiply alpha\n        MultiplyAlpha();\n\n        loaded = true;\n    }\n\n    png_destroy_info_struct(png_ptr, &info_ptr);\n    png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL);\n\n    // TODO: should probably check if the sRGB color space chunk is present\n    isSRGB = true;\n\n    return loaded;\n}\n\nvoid Image::MultiplyAlpha()\n{\n    if (pixelFormat == PixelFormat::R || pixelFormat == PixelFormat::RGB)\n    {\n        return;\n    }\n    else if (pixelFormat == PixelFormat::RA)\n    {\n        size_t pixelSize = 2;\n        for (size_t i = 0; i < width * height; i++)\n        {\n            float red = (float)data[i * pixelSize] / 255.0f;\n            float alpha = (float)data[i * pixelSize + 1] / 255.0f;\n            data[i * pixelSize] = (uint8_t)((red * alpha) * 255.0f);\n        }\n    }\n    else if (pixelFormat == PixelFormat::RGBA)\n    {\n        size_t pixelSize = 4;\n        for (size_t i = 0; i < width * height; i++)\n        {\n            float red = (float)data[i * pixelSize] / 255.0f;\n            float green = (float)data[i * pixelSize + 1] / 255.0f;\n            float blue = (float)data[i * pixelSize + 2] / 255.0f;\n            float alpha = (float)data[i * pixelSize + 3] / 255.0f;\n            data[i * pixelSize] = (uint8_t)((red * alpha) * 255.0f);\n            data[i * pixelSize + 1] = (uint8_t)((green * alpha) * 255.0f);\n            data[i * pixelSize + 2] = (uint8_t)((blue * alpha) * 255.0f);\n        }\n    }\n}\n"
  },
  {
    "path": "src/core/image.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <stdint.h>\n#include <string>\n#include <vector>\n\nenum class PixelFormat\n{\n    R = 0,  // intensity\n    RA,     // intensity alpha\n    RGB,\n    RGBA\n};\n\nstruct Image {\n    Image();\n    bool Load(const std::string& filename);\n    void MultiplyAlpha();\n\n    uint32_t width;\n    uint32_t height;\n    PixelFormat pixelFormat;\n    bool isSRGB;\n    std::vector<uint8_t> data;\n};\n"
  },
  {
    "path": "src/core/inputbuddy.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"inputbuddy.h\"\n\n#include <SDL2/SDL.h>\n\n#include \"log.h\"\n\nInputBuddy::InputBuddy()\n{\n    int numJoySticks = SDL_NumJoysticks();\n    if (numJoySticks > 0)\n    {\n        SDL_Joystick* joystick = SDL_JoystickOpen(0);\n        Log::I(\"Found joystick \\\"%s\\\"\\n\", SDL_JoystickName(joystick));\n    }\n    else\n    {\n        Log::I(\"No joystick found\\n\");\n    }\n}\n\nvoid InputBuddy::ProcessEvent(const SDL_Event& event)\n{\n    switch (event.type)\n    {\n    case SDL_QUIT:\n        quitCallback();\n        break;\n    case SDL_KEYDOWN:\n    case SDL_KEYUP:\n        {\n            SDL_Keycode keycode = event.key.keysym.sym;\n            uint16_t mod = event.key.keysym.mod;\n            bool down = (event.key.type == SDL_KEYDOWN);\n            auto iter = keyCallbackMap.find(keycode);\n            if (iter != keyCallbackMap.end() && !event.key.repeat)\n            {\n                iter->second(down, mod);\n            }\n        }\n        break;\n    case SDL_JOYAXISMOTION:\n        UpdateJoypadAxis(event.jaxis);\n        break;\n    case SDL_JOYHATMOTION:\n        UpdateJoypadHat(event.jhat);\n        break;\n    case SDL_JOYBUTTONDOWN:\n    case SDL_JOYBUTTONUP:\n        UpdateJoypadButton(event.jbutton);\n        break;\n\n    case SDL_WINDOWEVENT:\n        if (event.window.event == SDL_WINDOWEVENT_RESIZED)\n        {\n            resizeCallback(event.window.data1, event.window.data2);\n        }\n        break;\n\n    case SDL_MOUSEBUTTONDOWN:\n    case SDL_MOUSEBUTTONUP:\n        if (event.button.clicks == 1)\n        {\n            mouseButtonCallback(event.button.button, event.button.state == SDL_PRESSED,\n                                glm::ivec2(event.button.x, event.button.y));\n        }\n        break;\n    case SDL_MOUSEMOTION:\n        mouseMotionCallback(glm::ivec2(event.motion.x, event.motion.y), glm::ivec2(event.motion.xrel, event.motion.yrel));\n        break;\n    }\n}\n\nvoid InputBuddy::OnKey(Keycode key, const KeyCallback& cb)\n{\n    keyCallbackMap.insert(std::pair<Keycode, KeyCallback>(key, cb));\n}\n\nvoid InputBuddy::OnQuit(const VoidCallback& cb)\n{\n    quitCallback = cb;\n}\n\nvoid InputBuddy::OnResize(const ResizeCallback& cb)\n{\n    resizeCallback = cb;\n}\n\nvoid InputBuddy::OnMouseButton(const MouseButtonCallback& cb)\n{\n    mouseButtonCallback = cb;\n}\n\nvoid InputBuddy::OnMouseMotion(const MouseMotionCallback& cb)\n{\n    mouseMotionCallback = cb;\n}\n\nvoid InputBuddy::SetRelativeMouseMode(bool val)\n{\n    SDL_SetRelativeMouseMode(val ? SDL_TRUE : SDL_FALSE);\n}\n\nconst uint8_t LEFT_STICK_X_AXIS = 0;\nconst uint8_t LEFT_STICK_Y_AXIS = 1;\nconst uint8_t RIGHT_STICK_X_AXIS = 2;\nconst uint8_t RIGHT_STICK_Y_AXIS = 3;\nconst uint8_t LEFT_TRIGGER_AXIS = 4;\nconst uint8_t RIGHT_TRIGGER_AXIS = 5;\n\nstatic float Deadspot(float v)\n{\n    const float DEADSPOT = 0.15f;\n    return fabs(v) > DEADSPOT ? v : 0.0f;\n}\n\nvoid InputBuddy::UpdateJoypadAxis(const SDL_JoyAxisEvent& event)\n{\n    // only support one joypad\n    if (event.which != 0)\n    {\n        return;\n    }\n\n    const float AXIS_MAX = (float)SDL_JOYSTICK_AXIS_MAX;\n    switch (event.axis)\n    {\n    case LEFT_STICK_X_AXIS:\n        joypad.leftStick.x = Deadspot(event.value / AXIS_MAX);\n        break;\n    case LEFT_STICK_Y_AXIS:\n        joypad.leftStick.y = Deadspot(-event.value / AXIS_MAX);\n        break;\n    case RIGHT_STICK_X_AXIS:\n        joypad.rightStick.x = Deadspot(event.value / AXIS_MAX);\n        break;\n    case RIGHT_STICK_Y_AXIS:\n        joypad.rightStick.y = Deadspot(-event.value / AXIS_MAX);\n        break;\n    case LEFT_TRIGGER_AXIS:\n        joypad.leftTrigger = ((event.value / AXIS_MAX) * 0.5f) + 0.5f;  // (0, 1)\n        break;\n    case RIGHT_TRIGGER_AXIS:\n        joypad.rightTrigger = ((event.value / AXIS_MAX) * 0.5f) + 0.5f;  // (0, 1)\n        break;\n    }\n}\n\nconst uint8_t DPAD_HAT = 0;\n\nvoid InputBuddy::UpdateJoypadHat(const SDL_JoyHatEvent& event)\n{\n    // only support one joypad\n    if (event.which != 0)\n    {\n        return;\n    }\n\n    switch (event.hat)\n    {\n    case DPAD_HAT:\n        joypad.up = (event.value & SDL_HAT_UP) ? true : false;\n        joypad.right = (event.value & SDL_HAT_RIGHT) ? true : false;\n        joypad.down = (event.value & SDL_HAT_DOWN) ? true : false;\n        joypad.left = (event.value & SDL_HAT_LEFT) ? true : false;\n        break;\n    }\n}\n\nconst uint8_t A_BUTTON = 0;\nconst uint8_t B_BUTTON = 1;\nconst uint8_t X_BUTTON = 2;\nconst uint8_t Y_BUTTON = 3;\nconst uint8_t LEFT_BUMPER_BUTTON = 4;\nconst uint8_t RIGHT_BUMPER_BUTTON = 5;\nconst uint8_t MENU_BUTTON = 6;\nconst uint8_t VIEW_BUTTON = 7;\nconst uint8_t LEFT_STICK_BUTTON = 8;\nconst uint8_t RIGHT_STICK_BUTTON = 9;\n\nvoid InputBuddy::UpdateJoypadButton(const SDL_JoyButtonEvent& event)\n{\n    // only support one joypad\n    if (event.which != 0)\n    {\n        return;\n    }\n\n    switch (event.button)\n    {\n    case A_BUTTON:\n        joypad.a = event.state ? true : false;\n        break;\n    case B_BUTTON:\n        joypad.b = event.state ? true : false;\n        break;\n    case X_BUTTON:\n        joypad.x = event.state ? true : false;\n        break;\n    case Y_BUTTON:\n        joypad.y = event.state ? true : false;\n        break;\n    case LEFT_BUMPER_BUTTON:\n        joypad.lb = event.state ? true : false;\n        break;\n    case RIGHT_BUMPER_BUTTON:\n        joypad.rb = event.state ? true : false;\n        break;\n    case MENU_BUTTON:\n        joypad.menu = event.state ? true : false;\n        break;\n    case VIEW_BUTTON:\n        joypad.view = event.state ? true : false;\n        break;\n    case LEFT_STICK_BUTTON:\n        joypad.ls = event.state ? true : false;\n        break;\n    case RIGHT_STICK_BUTTON:\n        joypad.rs = event.state ? true : false;\n        break;\n    }\n}\n"
  },
  {
    "path": "src/core/inputbuddy.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <functional>\n#include <glm/glm.hpp>\n#include <map>\n#include <stdint.h>\n#include <string.h>\n\nunion SDL_Event;\nstruct SDL_JoyAxisEvent;\nstruct SDL_JoyHatEvent;\nstruct SDL_JoyButtonEvent;\n\nclass InputBuddy\n{\npublic:\n    using Keycode = int32_t;\n\n    InputBuddy();\n\n    using VoidCallback = std::function<void()>;\n    using KeyCallback = std::function<void(bool, uint16_t)>;\n    using ResizeCallback = std::function<void(int, int)>;\n    using MouseButtonCallback = std::function<void(uint8_t, bool, glm::ivec2)>; // button 1 = LEFT, 2 = MIDDLE, 3 = RIGHT\n    using MouseMotionCallback = std::function<void(glm::ivec2, glm::ivec2)>;\n\n    void ProcessEvent(const SDL_Event& event);\n\n    void OnKey(Keycode key, const KeyCallback& cb);\n    void OnQuit(const VoidCallback& cb);\n    void OnResize(const ResizeCallback& cb);\n    void OnMouseButton(const MouseButtonCallback& cb);\n    void OnMouseMotion(const MouseMotionCallback& cb);\n\n    void SetRelativeMouseMode(bool val);\n\n    // based on an xbox controler\n    struct Joypad\n    {\n        Joypad()\n        {\n            memset(this, 0, sizeof(Joypad));\n        }\n\n        glm::vec2 leftStick;\n        glm::vec2 rightStick;\n        float leftTrigger;\n        float rightTrigger;\n        bool down:1;\n        bool up:1;\n        bool left:1;\n        bool right:1;\n        bool view:1;\n        bool menu:1;\n        bool rs:1;\n        bool ls:1;\n        bool lb:1;\n        bool rb:1;\n        bool a:1;\n        bool b:1;\n        bool x:1;\n        bool y:1;\n    };\n\n    const Joypad& GetJoypad() const { return joypad; }\n\nprotected:\n    void UpdateJoypadAxis(const SDL_JoyAxisEvent& event);\n    void UpdateJoypadHat(const SDL_JoyHatEvent& event);\n    void UpdateJoypadButton(const SDL_JoyButtonEvent& event);\n\n    std::map<Keycode, KeyCallback> keyCallbackMap;\n    VoidCallback quitCallback;\n    ResizeCallback resizeCallback;\n    MouseButtonCallback mouseButtonCallback;\n    MouseMotionCallback mouseMotionCallback;\n    Joypad joypad;\n};\n"
  },
  {
    "path": "src/core/log.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"log.h\"\n#include <cstdio>\n#include <cstdarg>\n\n#ifdef __ANDROID__\n#include <android/log.h>\n#endif\n\nstatic Log::LogLevel level = Log::Verbose;\nstatic std::string appName = \"Core\";\n\n#ifdef __ANDROID__\nstatic int log_vprintf(int prio, const char *fmt, va_list args)\n{\n    char buffer[4096];\n    int rc = vsnprintf(buffer, sizeof(buffer), fmt, args);\n    __android_log_write(prio, appName.c_str(), buffer);\n    return rc;\n}\n#else\nstatic int log_vprintf(const char *fmt, va_list args)\n{\n    char buffer[4096];\n    int rc = vsnprintf(buffer, sizeof(buffer), fmt, args);\n    fwrite(buffer, rc, 1, stdout);\n    fflush(stdout);\n    return rc;\n}\n#endif\n\nint Log::printf(const char *fmt, ...)\n{\n#ifdef __ANDROID__\n    return 0;\n#else\n    char buffer[4096];\n    va_list args;\n    va_start(args, fmt);\n    int rc = vsnprintf(buffer, sizeof(buffer), fmt, args);\n    va_end(args);\n\n    fwrite(buffer, rc, 1, stdout);\n    fflush(stdout);\n\n    return rc;\n#endif\n}\n\nvoid Log::SetLevel(LogLevel levelIn)\n{\n    level = levelIn;\n}\n\nvoid Log::SetAppName(const std::string& appNameIn)\n{\n    appName = appNameIn;\n}\n\nvoid Log::V(const char *fmt, ...)\n{\n    if (level <= Log::Verbose)\n    {\n        Log::printf(\"[VERBOSE] \");\n        va_list args;\n        va_start(args, fmt);\n#ifdef __ANDROID__\n        log_vprintf(ANDROID_LOG_VERBOSE, fmt, args);\n#else\n        log_vprintf(fmt, args);\n#endif\n        va_end(args);\n    }\n}\n\nvoid Log::D(const char *fmt, ...)\n{\n    if (level <= Log::Debug)\n    {\n        Log::printf(\"[DEBUG] \");\n        va_list args;\n        va_start(args, fmt);\n#ifdef __ANDROID__\n        log_vprintf(ANDROID_LOG_DEBUG, fmt, args);\n#else\n        log_vprintf(fmt, args);\n#endif\n        va_end(args);\n    }\n}\n\nvoid Log::I(const char *fmt, ...)\n{\n    if (level <= Log::Info)\n    {\n        Log::printf(\"[INFO] \");\n        va_list args;\n        va_start(args, fmt);\n#ifdef __ANDROID__\n        log_vprintf(ANDROID_LOG_INFO, fmt, args);\n#else\n        log_vprintf(fmt, args);\n#endif\n        va_end(args);\n    }\n}\n\nvoid Log::W(const char *fmt, ...)\n{\n    if (level <= Log::Warning)\n    {\n        Log::printf(\"[WARNING] \");\n        va_list args;\n        va_start(args, fmt);\n#ifdef __ANDROID__\n        log_vprintf(ANDROID_LOG_WARN, fmt, args);\n#else\n        log_vprintf(fmt, args);\n#endif\n        va_end(args);\n    }\n}\n\nvoid Log::E(const char *fmt, ...)\n{\n    if (level <= Log::Error)\n    {\n        Log::printf(\"[ERROR] \");\n        va_list args;\n        va_start(args, fmt);\n#ifdef __ANDROID__\n        log_vprintf(ANDROID_LOG_ERROR, fmt, args);\n#else\n        log_vprintf(fmt, args);\n#endif\n        va_end(args);\n    }\n}\n\n\n"
  },
  {
    "path": "src/core/log.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <string>\n\nstruct Log\n{\n    enum LogLevel\n    {\n        Verbose = 0,\n        Debug = 1,\n        Info = 2,\n        Warning = 3,\n        Error = 4\n    };\n\n    static void SetLevel(LogLevel levelIn);\n    static void SetAppName(const std::string& appNameIn);\n\n    // verbose\n    static void V(const char *fmt, ...);\n\n    // debug\n    static void D(const char *fmt, ...);\n\n    // info\n    static void I(const char *fmt, ...);\n\n    // warning\n    static void W(const char *fmt, ...);\n\n    // error\n    static void E(const char *fmt, ...);\n\nprivate:\n    static int printf(const char *fmt, ...);\n};\n"
  },
  {
    "path": "src/core/optionparser.h",
    "content": "/*\n * The Lean Mean C++ Option Parser\n *\n * Copyright (C) 2012-2017 Matthias S. Benkmann\n *\n * The \"Software\" in the following 2 paragraphs refers to this file containing\n * the code to The Lean Mean C++ Option Parser.\n * The \"Software\" does NOT refer to any other files which you\n * may have received alongside this file (e.g. as part of a larger project that\n * incorporates The Lean Mean C++ Option Parser).\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software, to deal in the Software without restriction, including\n * without limitation the rights to use, copy, modify, merge, publish,\n * distribute, sublicense, and/or sell copies of the Software, and to permit\n * persons to whom the Software is furnished to do so, subject to the following\n * conditions:\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/*\n * NOTE: It is recommended that you read the processed HTML doxygen documentation\n * rather than this source. If you don't know doxygen, it's like javadoc for C++.\n * If you don't want to install doxygen you can find a copy of the processed\n * documentation at\n *\n * http://optionparser.sourceforge.net/\n *\n */\n\n/**\n * @file\n *\n * @brief This is the only file required to use The Lean Mean C++ Option Parser.\n *        Just \\#include it and you're set.\n *\n * The Lean Mean C++ Option Parser handles the program's command line arguments\n * (argc, argv).\n * It supports the short and long option formats of getopt(), getopt_long()\n * and getopt_long_only() but has a more convenient interface.\n *\n * @par Feedback:\n * Send questions, bug reports, feature requests etc. to: <tt><b>optionparser-feedback(a)lists.sourceforge.net</b></tt>\n *\n * @par Highlights:\n * <ul style=\"padding-left:1em;margin-left:0\">\n * <li> It is a header-only library. Just <code>\\#include \"optionparser.h\"</code> and you're set.\n * <li> It is freestanding. There are no dependencies whatsoever, not even the\n *      C or C++ standard library.\n * <li> It has a usage message formatter that supports column alignment and\n *      line wrapping. This aids localization because it adapts to\n *      translated strings that are shorter or longer (even if they contain\n *      Asian wide characters).\n * <li> Unlike getopt() and derivatives it doesn't force you to loop through\n *     options sequentially. Instead you can access options directly like this:\n *     <ul style=\"margin-top:.5em\">\n *     <li> Test for presence of a switch in the argument vector:\n *      @code if ( options[QUIET] ) ... @endcode\n *     <li> Evaluate --enable-foo/--disable-foo pair where the last one used wins:\n *     @code if ( options[FOO].last()->type() == DISABLE ) ... @endcode\n *     <li> Cumulative option (-v verbose, -vv more verbose, -vvv even more verbose):\n *     @code int verbosity = options[VERBOSE].count(); @endcode\n *     <li> Iterate over all --file=&lt;fname> arguments:\n *     @code for (Option* opt = options[FILE]; opt; opt = opt->next())\n *   fname = opt->arg; ... @endcode\n *     <li> If you really want to, you can still process all arguments in order:\n *     @code\n *   for (int i = 0; i < p.optionsCount(); ++i) {\n *     Option& opt = buffer[i];\n *     switch(opt.index()) {\n *       case HELP:    ...\n *       case VERBOSE: ...\n *       case FILE:    fname = opt.arg; ...\n *       case UNKNOWN: ...\n *     @endcode\n *     </ul>\n * </ul> @n\n * Despite these features the code size remains tiny.\n * It is smaller than <a href=\"http://uclibc.org\">uClibc</a>'s GNU getopt() and just a\n * couple 100 bytes larger than uClibc's SUSv3 getopt(). @n\n * (This does not include the usage formatter, of course. But you don't have to use that.)\n *\n * @par Download:\n * Tarball with examples and test programs:\n * <a style=\"font-size:larger;font-weight:bold\" href=\"http://sourceforge.net/projects/optionparser/files/optionparser-1.7.tar.gz/download\">optionparser-1.7.tar.gz</a> @n\n * Just the header (this is all you really need):\n * <a style=\"font-size:larger;font-weight:bold\" href=\"http://optionparser.sourceforge.net/optionparser.h\">optionparser.h</a>\n *\n * @par Changelog:\n * <b>Version 1.7:</b> Work on const-correctness. @n\n * <b>Version 1.6:</b> Fix for MSC compiler. @n\n * <b>Version 1.5:</b> Fixed 2 warnings about potentially uninitialized variables. @n\n *                     Added const version of Option::next(). @n\n * <b>Version 1.4:</b> Fixed 2 printUsage() bugs that messed up output with small COLUMNS values. @n\n * <b>Version 1.3:</b> Compatible with Microsoft Visual C++. @n\n * <b>Version 1.2:</b> Added @ref option::Option::namelen \"Option::namelen\" and removed the extraction\n *                     of short option characters into a special buffer. @n\n *                     Changed @ref option::Arg::Optional \"Arg::Optional\" to accept arguments if they are attached\n *                     rather than separate. This is what GNU getopt() does and how POSIX recommends\n *                     utilities should interpret their arguments.@n\n * <b>Version 1.1:</b> Optional mode with argument reordering as done by GNU getopt(), so that\n *                     options and non-options can be mixed. See\n *                     @ref option::Parser::parse() \"Parser::parse()\".\n *\n *\n * @par Example program:\n * (Note: @c option::* identifiers are links that take you to their documentation.)\n * @code\n * #error EXAMPLE SHORTENED FOR READABILITY. BETTER EXAMPLES ARE IN THE .TAR.GZ!\n * #include <iostream>\n * #include \"optionparser.h\"\n *\n * enum  optionIndex { UNKNOWN, HELP, PLUS };\n * const option::Descriptor usage[] =\n * {\n *  {UNKNOWN, 0,\"\" , \"\"    ,option::Arg::None, \"USAGE: example [options]\\n\\n\"\n *                                             \"Options:\" },\n *  {HELP,    0,\"\" , \"help\",option::Arg::None, \"  --help  \\tPrint usage and exit.\" },\n *  {PLUS,    0,\"p\", \"plus\",option::Arg::None, \"  --plus, -p  \\tIncrement count.\" },\n *  {UNKNOWN, 0,\"\" ,  \"\"   ,option::Arg::None, \"\\nExamples:\\n\"\n *                                             \"  example --unknown -- --this_is_no_option\\n\"\n *                                             \"  example -unk --plus -ppp file1 file2\\n\" },\n *  {0,0,0,0,0,0}\n * };\n *\n * int main(int argc, char* argv[])\n * {\n *   argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present\n *   option::Stats  stats(usage, argc, argv);\n *   option::Option options[stats.options_max], buffer[stats.buffer_max];\n *   option::Parser parse(usage, argc, argv, options, buffer);\n *\n *   if (parse.error())\n *     return 1;\n *\n *   if (options[HELP] || argc == 0) {\n *     option::printUsage(std::cout, usage);\n *     return 0;\n *   }\n *\n *   std::cout << \"--plus count: \" <<\n *     options[PLUS].count() << \"\\n\";\n *\n *   for (option::Option* opt = options[UNKNOWN]; opt; opt = opt->next())\n *     std::cout << \"Unknown option: \" << opt->name << \"\\n\";\n *\n *   for (int i = 0; i < parse.nonOptionsCount(); ++i)\n *     std::cout << \"Non-option #\" << i << \": \" << parse.nonOption(i) << \"\\n\";\n * }\n * @endcode\n *\n * @par Option syntax:\n * @li The Lean Mean C++ Option Parser follows POSIX <code>getopt()</code> conventions and supports\n *     GNU-style <code>getopt_long()</code> long options as well as Perl-style single-minus\n *     long options (<code>getopt_long_only()</code>).\n * @li short options have the format @c -X where @c X is any character that fits in a char.\n * @li short options can be grouped, i.e. <code>-X -Y</code> is equivalent to @c -XY.\n * @li a short option may take an argument either separate (<code>-X foo</code>) or\n *     attached (@c -Xfoo). You can make the parser accept the additional format @c -X=foo by\n *     registering @c X as a long option (in addition to being a short option) and\n *     enabling single-minus long options.\n * @li an argument-taking short option may be grouped if it is the last in the group, e.g.\n *     @c -ABCXfoo or <code> -ABCX foo </code> (@c foo is the argument to the @c -X option).\n * @li a lone minus character @c '-' is not treated as an option. It is customarily used where\n *     a file name is expected to refer to stdin or stdout.\n * @li long options have the format @c --option-name.\n * @li the option-name of a long option can be anything and include any characters.\n *     Even @c = characters will work, but don't do that.\n * @li [optional] long options may be abbreviated as long as the abbreviation is unambiguous.\n *     You can set a minimum length for abbreviations.\n * @li [optional] long options may begin with a single minus. The double minus form is always\n *     accepted, too.\n * @li a long option may take an argument either separate (<code> --option arg </code>) or\n *     attached (<code> --option=arg </code>). In the attached form the equals sign is mandatory.\n * @li an empty string can be passed as an attached long option argument: <code> --option-name= </code>.\n *     Note the distinction between an empty string as argument and no argument at all.\n * @li an empty string is permitted as separate argument to both long and short options.\n * @li Arguments to both short and long options may start with a @c '-' character. E.g.\n *     <code> -X-X </code>, <code>-X -X</code> or <code> --long-X=-X </code>. If @c -X\n *     and @c --long-X take an argument, that argument will be @c \"-X\" in all 3 cases.\n * @li If using the built-in @ref option::Arg::Optional \"Arg::Optional\", optional arguments must\n *     be attached.\n * @li the special option @c -- (i.e. without a name) terminates the list of\n *     options. Everything that follows is a non-option argument, even if it starts with\n *     a @c '-' character. The @c -- itself will not appear in the parse results.\n * @li the first argument that doesn't start with @c '-' or @c '--' and does not belong to\n *     a preceding argument-taking option, will terminate the option list and is the\n *     first non-option argument. All following command line arguments are treated as\n *     non-option arguments, even if they start with @c '-' . @n\n *     NOTE: This behaviour is mandated by POSIX, but GNU getopt() only honours this if it is\n *     explicitly requested (e.g. by setting POSIXLY_CORRECT). @n\n *     You can enable the GNU behaviour by passing @c true as first argument to\n *     e.g. @ref option::Parser::parse() \"Parser::parse()\".\n * @li Arguments that look like options (i.e. @c '-' followed by at least 1 character) but\n *     aren't, are NOT treated as non-option arguments. They are treated as unknown options and\n *     are collected into a list of unknown options for error reporting. @n\n *     This means that in order to pass a first non-option\n *     argument beginning with the minus character it is required to use the\n *     @c -- special option, e.g.\n *     @code\n *     program -x -- --strange-filename\n *     @endcode\n *     In this example, @c --strange-filename is a non-option argument. If the @c --\n *     were omitted, it would be treated as an unknown option. @n\n *     See @ref option::Descriptor::longopt for information on how to collect unknown options.\n *\n */\n\n#ifndef OPTIONPARSER_H_\n#define OPTIONPARSER_H_\n\n#ifdef _MSC_VER\n#include <intrin.h>\n#pragma intrinsic(_BitScanReverse)\n#endif\n\n/** @brief The namespace of The Lean Mean C++ Option Parser. */\nnamespace option\n{\n\n#ifdef _MSC_VER\nstruct MSC_Builtin_CLZ\n{\n  static int builtin_clz(unsigned x)\n  {\n    unsigned long index;\n    _BitScanReverse(&index, x);\n    return 32-index; // int is always 32bit on Windows, even for target x64\n  }\n};\n#define __builtin_clz(x) MSC_Builtin_CLZ::builtin_clz(x)\n#endif\n\nclass Option;\n\n/**\n * @brief Possible results when checking if an argument is valid for a certain option.\n *\n * In the case that no argument is provided for an option that takes an\n * optional argument, return codes @c ARG_OK and @c ARG_IGNORE are equivalent.\n */\nenum ArgStatus\n{\n  //! The option does not take an argument.\n  ARG_NONE,\n  //! The argument is acceptable for the option.\n  ARG_OK,\n  //! The argument is not acceptable but that's non-fatal because the option's argument is optional.\n  ARG_IGNORE,\n  //! The argument is not acceptable and that's fatal.\n  ARG_ILLEGAL\n};\n\n/**\n * @brief Signature of functions that check if an argument is valid for a certain type of option.\n *\n * Every Option has such a function assigned in its Descriptor.\n * @code\n * Descriptor usage[] = { {UNKNOWN, 0, \"\", \"\", Arg::None, \"\"}, ... };\n * @endcode\n *\n * A CheckArg function has the following signature:\n * @code ArgStatus CheckArg(const Option& option, bool msg); @endcode\n *\n * It is used to check if a potential argument would be acceptable for the option.\n * It will even be called if there is no argument. In that case @c option.arg will be @c NULL.\n *\n * If @c msg is @c true and the function determines that an argument is not acceptable and\n * that this is a fatal error, it should output a message to the user before\n * returning @ref ARG_ILLEGAL. If @c msg is @c false the function should remain silent (or you\n * will get duplicate messages).\n *\n * See @ref ArgStatus for the meaning of the return values.\n *\n * While you can provide your own functions,\n * often the following pre-defined checks (which never return @ref ARG_ILLEGAL) will suffice:\n *\n * @li @c Arg::None @copybrief Arg::None\n * @li @c Arg::Optional @copybrief Arg::Optional\n *\n */\ntypedef ArgStatus (*CheckArg)(const Option& option, bool msg);\n\n/**\n * @brief Describes an option, its help text (usage) and how it should be parsed.\n *\n * The main input when constructing an option::Parser is an array of Descriptors.\n\n * @par Example:\n * @code\n * enum OptionIndex {CREATE, ...};\n * enum OptionType {DISABLE, ENABLE, OTHER};\n *\n * const option::Descriptor usage[] = {\n *   { CREATE,                                            // index\n *     OTHER,                                             // type\n *     \"c\",                                               // shortopt\n *     \"create\",                                          // longopt\n *     Arg::None,                                         // check_arg\n *     \"--create  Tells the program to create something.\" // help\n *   }\n *   , ...\n * };\n * @endcode\n */\nstruct Descriptor\n{\n  /**\n   * @brief Index of this option's linked list in the array filled in by the parser.\n   *\n   * Command line options whose Descriptors have the same index will end up in the same\n   * linked list in the order in which they appear on the command line. If you have\n   * multiple long option aliases that refer to the same option, give their descriptors\n   * the same @c index.\n   *\n   * If you have options that mean exactly opposite things\n   * (e.g. @c --enable-foo and @c --disable-foo ), you should also give them the same\n   * @c index, but distinguish them through different values for @ref type.\n   * That way they end up in the same list and you can just take the last element of the\n   * list and use its type. This way you get the usual behaviour where switches later\n   * on the command line override earlier ones without having to code it manually.\n   *\n   * @par Tip:\n   * Use an enum rather than plain ints for better readability, as shown in the example\n   * at Descriptor.\n   */\n  const unsigned index;\n\n  /**\n   * @brief Used to distinguish between options with the same @ref index.\n   * See @ref index for details.\n   *\n   * It is recommended that you use an enum rather than a plain int to make your\n   * code more readable.\n   */\n  const int type;\n\n  /**\n   * @brief Each char in this string will be accepted as a short option character.\n   *\n   * The string must not include the minus character @c '-' or you'll get undefined\n   * behaviour.\n   *\n   * If this Descriptor should not have short option characters, use the empty\n   * string \"\". NULL is not permitted here!\n   *\n   * See @ref longopt for more information.\n   */\n  const char* const shortopt;\n\n  /**\n   * @brief The long option name (without the leading @c -- ).\n   *\n   * If this Descriptor should not have a long option name, use the empty\n   * string \"\". NULL is not permitted here!\n   *\n   * While @ref shortopt allows multiple short option characters, each\n   * Descriptor can have only a single long option name. If you have multiple\n   * long option names referring to the same option use separate Descriptors\n   * that have the same @ref index and @ref type. You may repeat\n   * short option characters in such an alias Descriptor but there's no need to.\n   *\n   * @par Dummy Descriptors:\n   * You can use dummy Descriptors with an\n   * empty string for both @ref shortopt and @ref longopt to add text to\n   * the usage that is not related to a specific option. See @ref help.\n   * The first dummy Descriptor will be used for unknown options (see below).\n   *\n   * @par Unknown Option Descriptor:\n   * The first dummy Descriptor in the list of Descriptors,\n   * whose @ref shortopt and @ref longopt are both the empty string, will be used\n   * as the Descriptor for unknown options. An unknown option is a string in\n   * the argument vector that is not a lone minus @c '-' but starts with a minus\n   * character and does not match any Descriptor's @ref shortopt or @ref longopt. @n\n   * Note that the dummy descriptor's @ref check_arg function @e will be called and\n   * its return value will be evaluated as usual. I.e. if it returns @ref ARG_ILLEGAL\n   * the parsing will be aborted with <code>Parser::error()==true</code>. @n\n   * if @c check_arg does not return @ref ARG_ILLEGAL the descriptor's\n   * @ref index @e will be used to pick the linked list into which\n   * to put the unknown option. @n\n   * If there is no dummy descriptor, unknown options will be dropped silently.\n   *\n   */\n  const char* const longopt;\n\n  /**\n   * @brief For each option that matches @ref shortopt or @ref longopt this function\n   * will be called to check a potential argument to the option.\n   *\n   * This function will be called even if there is no potential argument. In that case\n   * it will be passed @c NULL as @c arg parameter. Do not confuse this with the empty\n   * string.\n   *\n   * See @ref CheckArg for more information.\n   */\n  const CheckArg check_arg;\n\n  /**\n   * @brief The usage text associated with the options in this Descriptor.\n   *\n   * You can use option::printUsage() to format your usage message based on\n   * the @c help texts. You can use dummy Descriptors where\n   * @ref shortopt and @ref longopt are both the empty string to add text to\n   * the usage that is not related to a specific option.\n   *\n   * See option::printUsage() for special formatting characters you can use in\n   * @c help to get a column layout.\n   *\n   * @attention\n   * Must be UTF-8-encoded. If your compiler supports C++11 you can use the \"u8\"\n   * prefix to make sure string literals are properly encoded.\n   */\n  const char* help;\n};\n\n/**\n * @brief A parsed option from the command line together with its argument if it has one.\n *\n * The Parser chains all parsed options with the same Descriptor::index together\n * to form a linked list. This allows you to easily implement all of the common ways\n * of handling repeated options and enable/disable pairs.\n *\n * @li Test for presence of a switch in the argument vector:\n *      @code if ( options[QUIET] ) ... @endcode\n * @li Evaluate --enable-foo/--disable-foo pair where the last one used wins:\n *     @code if ( options[FOO].last()->type() == DISABLE ) ... @endcode\n * @li Cumulative option (-v verbose, -vv more verbose, -vvv even more verbose):\n *     @code int verbosity = options[VERBOSE].count(); @endcode\n * @li Iterate over all --file=&lt;fname> arguments:\n *     @code for (Option* opt = options[FILE]; opt; opt = opt->next())\n *   fname = opt->arg; ... @endcode\n */\nclass Option\n{\n  Option* next_;\n  Option* prev_;\npublic:\n  /**\n   * @brief Pointer to this Option's Descriptor.\n   *\n   * Remember that the first dummy descriptor (see @ref Descriptor::longopt) is used\n   * for unknown options.\n   *\n   * @attention\n   * @c desc==NULL signals that this Option is unused. This is the default state of\n   * elements in the result array. You don't need to test @c desc explicitly. You\n   * can simply write something like this:\n   * @code\n   * if (options[CREATE])\n   * {\n   *   ...\n   * }\n   * @endcode\n   * This works because of <code> operator const Option*() </code>.\n   */\n  const Descriptor* desc;\n\n  /**\n   * @brief The name of the option as used on the command line.\n   *\n   * The main purpose of this string is to be presented to the user in messages.\n   *\n   * In the case of a long option, this is the actual @c argv pointer, i.e. the first\n   * character is a '-'. In the case of a short option this points to the option\n   * character within the @c argv string.\n   *\n   * Note that in the case of a short option group or an attached option argument, this\n   * string will contain additional characters following the actual name. Use @ref namelen\n   * to filter out the actual option name only.\n   *\n   */\n  const char* name;\n\n  /**\n   * @brief Pointer to this Option's argument (if any).\n   *\n   * NULL if this option has no argument. Do not confuse this with the empty string which\n   * is a valid argument.\n   */\n  const char* arg;\n\n  /**\n   * @brief The length of the option @ref name.\n   *\n   * Because @ref name points into the actual @c argv string, the option name may be\n   * followed by more characters (e.g. other short options in the same short option group).\n   * This value is the number of bytes (not characters!) that are part of the actual name.\n   *\n   * For a short option, this length is always 1. For a long option this length is always\n   * at least 2 if single minus long options are permitted and at least 3 if they are disabled.\n   *\n   * @note\n   * In the pathological case of a minus within a short option group (e.g. @c -xf-z), this\n   * length is incorrect, because this case will be misinterpreted as a long option and the\n   * name will therefore extend to the string's 0-terminator or a following '=\" character\n   * if there is one. This is irrelevant for most uses of @ref name and @c namelen. If you\n   * really need to distinguish the case of a long and a short option, compare @ref name to\n   * the @c argv pointers. A long option's @c name is always identical to one of them,\n   * whereas a short option's is never.\n   */\n  int namelen;\n\n  /**\n   * @brief Returns Descriptor::type of this Option's Descriptor, or 0 if this Option\n   * is invalid (unused).\n   *\n   * Because this method (and last(), too) can be used even on unused Options with desc==0, you can (provided\n   * you arrange your types properly) switch on type() without testing validity first.\n   * @code\n   * enum OptionType { UNUSED=0, DISABLED=0, ENABLED=1 };\n   * enum OptionIndex { FOO };\n   * const Descriptor usage[] = {\n   *   { FOO, ENABLED,  \"\", \"enable-foo\",  Arg::None, 0 },\n   *   { FOO, DISABLED, \"\", \"disable-foo\", Arg::None, 0 },\n   *   { 0, 0, 0, 0, 0, 0 } };\n   * ...\n   * switch(options[FOO].last()->type()) // no validity check required!\n   * {\n   *   case ENABLED: ...\n   *   case DISABLED: ...  // UNUSED==DISABLED !\n   * }\n   * @endcode\n   */\n  int type() const\n  {\n    return desc == 0 ? 0 : desc->type;\n  }\n\n  /**\n   * @brief Returns Descriptor::index of this Option's Descriptor, or -1 if this Option\n   * is invalid (unused).\n   */\n  int index() const\n  {\n    return desc == 0 ? -1 : (int)desc->index;\n  }\n\n  /**\n   * @brief Returns the number of times this Option (or others with the same Descriptor::index)\n   * occurs in the argument vector.\n   *\n   * This corresponds to the number of elements in the linked list this Option is part of.\n   * It doesn't matter on which element you call count(). The return value is always the same.\n   *\n   * Use this to implement cumulative options, such as -v, -vv, -vvv for\n   * different verbosity levels.\n   *\n   * Returns 0 when called for an unused/invalid option.\n   */\n  int count() const\n  {\n    int c = (desc == 0 ? 0 : 1);\n    const Option* p = first();\n    while (!p->isLast())\n    {\n      ++c;\n      p = p->next_;\n    };\n    return c;\n  }\n\n  /**\n   * @brief Returns true iff this is the first element of the linked list.\n   *\n   * The first element in the linked list is the first option on the command line\n   * that has the respective Descriptor::index value.\n   *\n   * Returns true for an unused/invalid option.\n   */\n  bool isFirst() const\n  {\n    return isTagged(prev_);\n  }\n\n  /**\n   * @brief Returns true iff this is the last element of the linked list.\n   *\n   * The last element in the linked list is the last option on the command line\n   * that has the respective Descriptor::index value.\n   *\n   * Returns true for an unused/invalid option.\n   */\n  bool isLast() const\n  {\n    return isTagged(next_);\n  }\n\n  /**\n   * @brief Returns a pointer to the first element of the linked list.\n   *\n   * Use this when you want the first occurrence of an option on the command line to\n   * take precedence. Note that this is not the way most programs handle options.\n   * You should probably be using last() instead.\n   *\n   * @note\n   * This method may be called on an unused/invalid option and will return a pointer to the\n   * option itself.\n   */\n  Option* first()\n  {\n    Option* p = this;\n    while (!p->isFirst())\n      p = p->prev_;\n    return p;\n  }\n\n  /**\n  * const version of Option::first().\n  */\n  const Option* first() const\n  {\n    return const_cast<Option*>(this)->first();\n  }\n\n  /**\n   * @brief Returns a pointer to the last element of the linked list.\n   *\n   * Use this when you want the last occurrence of an option on the command line to\n   * take precedence. This is the most common way of handling conflicting options.\n   *\n   * @note\n   * This method may be called on an unused/invalid option and will return a pointer to the\n   * option itself.\n   *\n   * @par Tip:\n   * If you have options with opposite meanings (e.g. @c --enable-foo and @c --disable-foo), you\n   * can assign them the same Descriptor::index to get them into the same list. Distinguish them by\n   * Descriptor::type and all you have to do is check <code> last()->type() </code> to get\n   * the state listed last on the command line.\n   */\n  Option* last()\n  {\n    return first()->prevwrap();\n  }\n\n  /**\n  * const version of Option::last().\n  */\n  const Option* last() const\n  {\n    return first()->prevwrap();\n  }\n\n  /**\n   * @brief Returns a pointer to the previous element of the linked list or NULL if\n   * called on first().\n   *\n   * If called on first() this method returns NULL. Otherwise it will return the\n   * option with the same Descriptor::index that precedes this option on the command\n   * line.\n   */\n  Option* prev()\n  {\n    return isFirst() ? 0 : prev_;\n  }\n\n  /**\n   * @brief Returns a pointer to the previous element of the linked list with wrap-around from\n   * first() to last().\n   *\n   * If called on first() this method returns last(). Otherwise it will return the\n   * option with the same Descriptor::index that precedes this option on the command\n   * line.\n   */\n  Option* prevwrap()\n  {\n    return untag(prev_);\n  }\n\n  /**\n  * const version of Option::prevwrap().\n  */\n  const Option* prevwrap() const\n  {\n    return untag(prev_);\n  }\n\n  /**\n   * @brief Returns a pointer to the next element of the linked list or NULL if called\n   * on last().\n   *\n   * If called on last() this method returns NULL. Otherwise it will return the\n   * option with the same Descriptor::index that follows this option on the command\n   * line.\n   */\n  Option* next()\n  {\n    return isLast() ? 0 : next_;\n  }\n\n  /**\n  * const version of Option::next().\n  */\n  const Option* next() const\n  {\n    return isLast() ? 0 : next_;\n  }\n\n  /**\n   * @brief Returns a pointer to the next element of the linked list with wrap-around from\n   * last() to first().\n   *\n   * If called on last() this method returns first(). Otherwise it will return the\n   * option with the same Descriptor::index that follows this option on the command\n   * line.\n   */\n  Option* nextwrap()\n  {\n    return untag(next_);\n  }\n\n  /**\n   * @brief Makes @c new_last the new last() by chaining it into the list after last().\n   *\n   * It doesn't matter which element you call append() on. The new element will always\n   * be appended to last().\n   *\n   * @attention\n   * @c new_last must not yet be part of a list, or that list will become corrupted, because\n   * this method does not unchain @c new_last from an existing list.\n   */\n  void append(Option* new_last)\n  {\n    Option* p = last();\n    Option* f = first();\n    p->next_ = new_last;\n    new_last->prev_ = p;\n    new_last->next_ = tag(f);\n    f->prev_ = tag(new_last);\n  }\n\n  /**\n   * @brief Casts from Option to const Option* but only if this Option is valid.\n   *\n   * If this Option is valid (i.e. @c desc!=NULL), returns this.\n   * Otherwise returns NULL. This allows testing an Option directly\n   * in an if-clause to see if it is used:\n   * @code\n   * if (options[CREATE])\n   * {\n   *   ...\n   * }\n   * @endcode\n   * It also allows you to write loops like this:\n   * @code for (Option* opt = options[FILE]; opt; opt = opt->next())\n   *   fname = opt->arg; ... @endcode\n   */\n  operator const Option*() const\n  {\n    return desc ? this : 0;\n  }\n\n  /**\n   * @brief Casts from Option to Option* but only if this Option is valid.\n   *\n   * If this Option is valid (i.e. @c desc!=NULL), returns this.\n   * Otherwise returns NULL. This allows testing an Option directly\n   * in an if-clause to see if it is used:\n   * @code\n   * if (options[CREATE])\n   * {\n   *   ...\n   * }\n   * @endcode\n   * It also allows you to write loops like this:\n   * @code for (Option* opt = options[FILE]; opt; opt = opt->next())\n   *   fname = opt->arg; ... @endcode\n   */\n  operator Option*()\n  {\n    return desc ? this : 0;\n  }\n\n  /**\n   * @brief Creates a new Option that is a one-element linked list and has NULL\n   * @ref desc, @ref name, @ref arg and @ref namelen.\n   */\n  Option() :\n      desc(0), name(0), arg(0), namelen(0)\n  {\n    prev_ = tag(this);\n    next_ = tag(this);\n  }\n\n  /**\n   * @brief Creates a new Option that is a one-element linked list and has the given\n   * values for @ref desc, @ref name and @ref arg.\n   *\n   * If @c name_ points at a character other than '-' it will be assumed to refer to a\n   * short option and @ref namelen will be set to 1. Otherwise the length will extend to\n   * the first '=' character or the string's 0-terminator.\n   */\n  Option(const Descriptor* desc_, const char* name_, const char* arg_)\n  {\n    init(desc_, name_, arg_);\n  }\n\n  /**\n   * @brief Makes @c *this a copy of @c orig except for the linked list pointers.\n   *\n   * After this operation @c *this will be a one-element linked list.\n   */\n  void operator=(const Option& orig)\n  {\n    init(orig.desc, orig.name, orig.arg);\n  }\n\n  /**\n   * @brief Makes @c *this a copy of @c orig except for the linked list pointers.\n   *\n   * After this operation @c *this will be a one-element linked list.\n   */\n  Option(const Option& orig)\n  {\n    init(orig.desc, orig.name, orig.arg);\n  }\n\nprivate:\n  /**\n   * @internal\n   * @brief Sets the fields of this Option to the given values (extracting @c name if necessary).\n   *\n   * If @c name_ points at a character other than '-' it will be assumed to refer to a\n   * short option and @ref namelen will be set to 1. Otherwise the length will extend to\n   * the first '=' character or the string's 0-terminator.\n   */\n  void init(const Descriptor* desc_, const char* name_, const char* arg_)\n  {\n    desc = desc_;\n    name = name_;\n    arg = arg_;\n    prev_ = tag(this);\n    next_ = tag(this);\n    namelen = 0;\n    if (name == 0)\n      return;\n    namelen = 1;\n    if (name[0] != '-')\n      return;\n    while (name[namelen] != 0 && name[namelen] != '=')\n      ++namelen;\n  }\n\n  static Option* tag(Option* ptr)\n  {\n    return (Option*) ((unsigned long long) ptr | 1);\n  }\n\n  static Option* untag(Option* ptr)\n  {\n    return (Option*) ((unsigned long long) ptr & ~1ull);\n  }\n\n  static bool isTagged(Option* ptr)\n  {\n    return ((unsigned long long) ptr & 1);\n  }\n};\n\n/**\n * @brief Functions for checking the validity of option arguments.\n *\n * @copydetails CheckArg\n *\n * The following example code\n * can serve as starting place for writing your own more complex CheckArg functions:\n * @code\n * struct Arg: public option::Arg\n * {\n *   static void printError(const char* msg1, const option::Option& opt, const char* msg2)\n *   {\n *     fprintf(stderr, \"ERROR: %s\", msg1);\n *     fwrite(opt.name, opt.namelen, 1, stderr);\n *     fprintf(stderr, \"%s\", msg2);\n *   }\n *\n *   static option::ArgStatus Unknown(const option::Option& option, bool msg)\n *   {\n *     if (msg) printError(\"Unknown option '\", option, \"'\\n\");\n *     return option::ARG_ILLEGAL;\n *   }\n *\n *   static option::ArgStatus Required(const option::Option& option, bool msg)\n *   {\n *     if (option.arg != 0)\n *       return option::ARG_OK;\n *\n *     if (msg) printError(\"Option '\", option, \"' requires an argument\\n\");\n *     return option::ARG_ILLEGAL;\n *   }\n *\n *   static option::ArgStatus NonEmpty(const option::Option& option, bool msg)\n *   {\n *     if (option.arg != 0 && option.arg[0] != 0)\n *       return option::ARG_OK;\n *\n *     if (msg) printError(\"Option '\", option, \"' requires a non-empty argument\\n\");\n *     return option::ARG_ILLEGAL;\n *   }\n *\n *   static option::ArgStatus Numeric(const option::Option& option, bool msg)\n *   {\n *     char* endptr = 0;\n *     if (option.arg != 0 && strtol(option.arg, &endptr, 10)){};\n *     if (endptr != option.arg && *endptr == 0)\n *       return option::ARG_OK;\n *\n *     if (msg) printError(\"Option '\", option, \"' requires a numeric argument\\n\");\n *     return option::ARG_ILLEGAL;\n *   }\n * };\n * @endcode\n */\nstruct Arg\n{\n  //! @brief For options that don't take an argument: Returns ARG_NONE.\n  static ArgStatus None(const Option&, bool)\n  {\n    return ARG_NONE;\n  }\n\n  //! @brief Returns ARG_OK if the argument is attached and ARG_IGNORE otherwise.\n  static ArgStatus Optional(const Option& option, bool)\n  {\n    if (option.arg && option.name[option.namelen] != 0)\n      return ARG_OK;\n    else\n      return ARG_IGNORE;\n  }\n};\n\n/**\n * @brief Determines the minimum lengths of the buffer and options arrays used for Parser.\n *\n * Because Parser doesn't use dynamic memory its output arrays have to be pre-allocated.\n * If you don't want to use fixed size arrays (which may turn out too small, causing\n * command line arguments to be dropped), you can use Stats to determine the correct sizes.\n * Stats work cumulative. You can first pass in your default options and then the real\n * options and afterwards the counts will reflect the union.\n */\nstruct Stats\n{\n  /**\n   * @brief Number of elements needed for a @c buffer[] array to be used for\n   * @ref Parser::parse() \"parsing\" the same argument vectors that were fed\n   * into this Stats object.\n   *\n   * @note\n   * This number is always 1 greater than the actual number needed, to give\n   * you a sentinel element.\n   */\n  unsigned buffer_max;\n\n  /**\n   * @brief Number of elements needed for an @c options[] array to be used for\n   * @ref Parser::parse() \"parsing\" the same argument vectors that were fed\n   * into this Stats object.\n   *\n   * @note\n   * @li This number is always 1 greater than the actual number needed, to give\n   * you a sentinel element.\n   * @li This number depends only on the @c usage, not the argument vectors, because\n   * the @c options array needs exactly one slot for each possible Descriptor::index.\n   */\n  unsigned options_max;\n\n  /**\n   * @brief Creates a Stats object with counts set to 1 (for the sentinel element).\n   */\n  Stats() :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n  }\n\n  /**\n   * @brief Creates a new Stats object and immediately updates it for the\n   * given @c usage and argument vector. You may pass 0 for @c argc and/or @c argv,\n   * if you just want to update @ref options_max.\n   *\n   * @note\n   * The calls to Stats methods must match the later calls to Parser methods.\n   * See Parser::parse() for the meaning of the arguments.\n   */\n  Stats(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, //\n        bool single_minus_longopt = false) :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n    add(gnu, usage, argc, argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief Stats(...) with non-const argv.\n  Stats(bool gnu, const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, //\n        bool single_minus_longopt = false) :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n    add(gnu, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief POSIX Stats(...) (gnu==false).\n  Stats(const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, //\n        bool single_minus_longopt = false) :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n    add(false, usage, argc, argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief POSIX Stats(...) (gnu==false) with non-const argv.\n  Stats(const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, //\n        bool single_minus_longopt = false) :\n      buffer_max(1), options_max(1) // 1 more than necessary as sentinel\n  {\n    add(false, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt);\n  }\n\n  /**\n   * @brief Updates this Stats object for the\n   * given @c usage and argument vector. You may pass 0 for @c argc and/or @c argv,\n   * if you just want to update @ref options_max.\n   *\n   * @note\n   * The calls to Stats methods must match the later calls to Parser methods.\n   * See Parser::parse() for the meaning of the arguments.\n   */\n  void add(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, //\n           bool single_minus_longopt = false);\n\n  //! @brief add() with non-const argv.\n  void add(bool gnu, const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, //\n           bool single_minus_longopt = false)\n  {\n    add(gnu, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief POSIX add() (gnu==false).\n  void add(const Descriptor usage[], int argc, const char** argv, int min_abbr_len = 0, //\n           bool single_minus_longopt = false)\n  {\n    add(false, usage, argc, argv, min_abbr_len, single_minus_longopt);\n  }\n\n  //! @brief POSIX add() (gnu==false) with non-const argv.\n  void add(const Descriptor usage[], int argc, char** argv, int min_abbr_len = 0, //\n           bool single_minus_longopt = false)\n  {\n    add(false, usage, argc, (const char**) argv, min_abbr_len, single_minus_longopt);\n  }\nprivate:\n  class CountOptionsAction;\n};\n\n/**\n * @brief Checks argument vectors for validity and parses them into data\n * structures that are easier to work with.\n *\n * @par Example:\n * @code\n * int main(int argc, char* argv[])\n * {\n *   argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present\n *   option::Stats  stats(usage, argc, argv);\n *   option::Option options[stats.options_max], buffer[stats.buffer_max];\n *   option::Parser parse(usage, argc, argv, options, buffer);\n *\n *   if (parse.error())\n *     return 1;\n *\n *   if (options[HELP])\n *   ...\n * @endcode\n */\nclass Parser\n{\n  int op_count; //!< @internal @brief see optionsCount()\n  int nonop_count; //!< @internal @brief see nonOptionsCount()\n  const char** nonop_args; //!< @internal @brief see nonOptions()\n  bool err; //!< @internal @brief see error()\npublic:\n\n  /**\n   * @brief Creates a new Parser.\n   */\n  Parser() :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n  }\n\n  /**\n   * @brief Creates a new Parser and immediately parses the given argument vector.\n   * @copydetails parse()\n   */\n  Parser(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[],\n         int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n    parse(gnu, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief Parser(...) with non-const argv.\n  Parser(bool gnu, const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[],\n         int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1) :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n    parse(gnu, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief POSIX Parser(...) (gnu==false).\n  Parser(const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[], int min_abbr_len = 0,\n         bool single_minus_longopt = false, int bufmax = -1) :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n    parse(false, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief POSIX Parser(...) (gnu==false) with non-const argv.\n  Parser(const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[], int min_abbr_len = 0,\n         bool single_minus_longopt = false, int bufmax = -1) :\n      op_count(0), nonop_count(0), nonop_args(0), err(false)\n  {\n    parse(false, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  /**\n   * @brief Parses the given argument vector.\n   *\n   * @param gnu if true, parse() will not stop at the first non-option argument. Instead it will\n   *            reorder arguments so that all non-options are at the end. This is the default behaviour\n   *            of GNU getopt() but is not conforming to POSIX. @n\n   *            Note, that once the argument vector has been reordered, the @c gnu flag will have\n   *            no further effect on this argument vector. So it is enough to pass @c gnu==true when\n   *            creating Stats.\n   * @param usage Array of Descriptor objects that describe the options to support. The last entry\n   *              of this array must have 0 in all fields.\n   * @param argc The number of elements from @c argv that are to be parsed. If you pass -1, the number\n   *             will be determined automatically. In that case the @c argv list must end with a NULL\n   *             pointer.\n   * @param argv The arguments to be parsed. If you pass -1 as @c argc the last pointer in the @c argv\n   *             list must be NULL to mark the end.\n   * @param options Each entry is the first element of a linked list of Options. Each new option\n   *                that is parsed will be appended to the list specified by that Option's\n   *                Descriptor::index. If an entry is not yet used (i.e. the Option is invalid),\n   *                it will be replaced rather than appended to. @n\n   *                The minimum length of this array is the greatest Descriptor::index value that\n   *                occurs in @c usage @e PLUS ONE.\n   * @param buffer Each argument that is successfully parsed (including unknown arguments, if they\n   *        have a Descriptor whose CheckArg does not return @ref ARG_ILLEGAL) will be stored in this\n   *        array. parse() scans the array for the first invalid entry and begins writing at that\n   *        index. You can pass @c bufmax to limit the number of options stored.\n   * @param min_abbr_len Passing a value <code> min_abbr_len > 0 </code> enables abbreviated long\n   *               options. The parser will match a prefix of a long option as if it was\n   *               the full long option (e.g. @c --foob=10 will be interpreted as if it was\n   *               @c --foobar=10 ), as long as the prefix has at least @c min_abbr_len characters\n   *               (not counting the @c -- ) and is unambiguous.\n   *               @n Be careful if combining @c min_abbr_len=1 with @c single_minus_longopt=true\n   *               because the ambiguity check does not consider short options and abbreviated\n   *               single minus long options will take precedence over short options.\n   * @param single_minus_longopt Passing @c true for this option allows long options to begin with\n   *               a single minus. The double minus form will still be recognized. Note that\n   *               single minus long options take precedence over short options and short option\n   *               groups. E.g. @c -file would be interpreted as @c --file and not as\n   *               <code> -f -i -l -e </code> (assuming a long option named @c \"file\" exists).\n   * @param bufmax The greatest index in the @c buffer[] array that parse() will write to is\n   *               @c bufmax-1. If there are more options, they will be processed (in particular\n   *               their CheckArg will be called) but not stored. @n\n   *               If you used Stats::buffer_max to dimension this array, you can pass\n   *               -1 (or not pass @c bufmax at all) which tells parse() that the buffer is\n   *               \"large enough\".\n   * @attention\n   * Remember that @c options and @c buffer store Option @e objects, not pointers. Therefore it\n   * is not possible for the same object to be in both arrays. For those options that are found in\n   * both @c buffer[] and @c options[] the respective objects are independent copies. And only the\n   * objects in @c options[] are properly linked via Option::next() and Option::prev().\n   * You can iterate over @c buffer[] to\n   * process all options in the order they appear in the argument vector, but if you want access to\n   * the other Options with the same Descriptor::index, then you @e must access the linked list via\n   * @c options[]. You can get the linked list in options from a buffer object via something like\n   * @c options[buffer[i].index()].\n   */\n  void parse(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[],\n             int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1);\n\n  //! @brief parse() with non-const argv.\n  void parse(bool gnu, const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[],\n             int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1)\n  {\n    parse(gnu, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief POSIX parse() (gnu==false).\n  void parse(const Descriptor usage[], int argc, const char** argv, Option options[], Option buffer[],\n             int min_abbr_len = 0, bool single_minus_longopt = false, int bufmax = -1)\n  {\n    parse(false, usage, argc, argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  //! @brief POSIX parse() (gnu==false) with non-const argv.\n  void parse(const Descriptor usage[], int argc, char** argv, Option options[], Option buffer[], int min_abbr_len = 0,\n             bool single_minus_longopt = false, int bufmax = -1)\n  {\n    parse(false, usage, argc, (const char**) argv, options, buffer, min_abbr_len, single_minus_longopt, bufmax);\n  }\n\n  /**\n   * @brief Returns the number of valid Option objects in @c buffer[].\n   *\n   * @note\n   * @li The returned value always reflects the number of Options in the buffer[] array used for\n   * the most recent call to parse().\n   * @li The count (and the buffer[]) includes unknown options if they are collected\n   * (see Descriptor::longopt).\n   */\n  int optionsCount()\n  {\n    return op_count;\n  }\n\n  /**\n   * @brief Returns the number of non-option arguments that remained at the end of the\n   * most recent parse() that actually encountered non-option arguments.\n   *\n   * @note\n   * A parse() that does not encounter non-option arguments will leave this value\n   * as well as nonOptions() undisturbed. This means you can feed the Parser a\n   * default argument vector that contains non-option arguments (e.g. a default filename).\n   * Then you feed it the actual arguments from the user. If the user has supplied at\n   * least one non-option argument, all of the non-option arguments from the default\n   * disappear and are replaced by the user's non-option arguments. However, if the\n   * user does not supply any non-option arguments the defaults will still be in\n   * effect.\n   */\n  int nonOptionsCount()\n  {\n    return nonop_count;\n  }\n\n  /**\n   * @brief Returns a pointer to an array of non-option arguments (only valid\n   * if <code>nonOptionsCount() >0 </code>).\n   *\n   * @note\n   * @li parse() does not copy arguments, so this pointer points into the actual argument\n   * vector as passed to parse().\n   * @li As explained at nonOptionsCount() this pointer is only changed by parse() calls\n   * that actually encounter non-option arguments. A parse() call that encounters only\n   * options, will not change nonOptions().\n   */\n  const char** nonOptions()\n  {\n    return nonop_args;\n  }\n\n  /**\n   * @brief Returns <b><code>nonOptions()[i]</code></b> (@e without checking if i is in range!).\n   */\n  const char* nonOption(int i)\n  {\n    return nonOptions()[i];\n  }\n\n  /**\n   * @brief Returns @c true if an unrecoverable error occurred while parsing options.\n   *\n   * An illegal argument to an option (i.e. CheckArg returns @ref ARG_ILLEGAL) is an\n   * unrecoverable error that aborts the parse. Unknown options are only an error if\n   * their CheckArg function returns @ref ARG_ILLEGAL. Otherwise they are collected.\n   * In that case if you want to exit the program if either an illegal argument\n   * or an unknown option has been passed, use code like this\n   *\n   * @code\n   * if (parser.error() || options[UNKNOWN])\n   *   exit(1);\n   * @endcode\n   *\n   */\n  bool error()\n  {\n    return err;\n  }\n\nprivate:\n  friend struct Stats;\n  class StoreOptionAction;\n  struct Action;\n\n  /**\n   * @internal\n   * @brief This is the core function that does all the parsing.\n   * @retval false iff an unrecoverable error occurred.\n   */\n  static bool workhorse(bool gnu, const Descriptor usage[], int numargs, const char** args, Action& action,\n                        bool single_minus_longopt, bool print_errors, int min_abbr_len);\n\n  /**\n   * @internal\n   * @brief Returns true iff @c st1 is a prefix of @c st2 and\n   * in case @c st2 is longer than @c st1, then\n   * the first additional character is '='.\n   *\n   * @par Examples:\n   * @code\n   * streq(\"foo\", \"foo=bar\") == true\n   * streq(\"foo\", \"foobar\")  == false\n   * streq(\"foo\", \"foo\")     == true\n   * streq(\"foo=bar\", \"foo\") == false\n   * @endcode\n   */\n  static bool streq(const char* st1, const char* st2)\n  {\n    while (*st1 != 0)\n      if (*st1++ != *st2++)\n        return false;\n    return (*st2 == 0 || *st2 == '=');\n  }\n\n  /**\n   * @internal\n   * @brief Like streq() but handles abbreviations.\n   *\n   * Returns true iff @c st1 and @c st2 have a common\n   * prefix with the following properties:\n   * @li (if min > 0) its length is at least @c min characters or the same length as @c st1 (whichever is smaller).\n   * @li (if min <= 0) its length is the same as that of @c st1\n   * @li within @c st2 the character following the common prefix is either '=' or end-of-string.\n   *\n   * Examples:\n   * @code\n   * streqabbr(\"foo\", \"foo=bar\",<anything>) == true\n   * streqabbr(\"foo\", \"fo=bar\" , 2) == true\n   * streqabbr(\"foo\", \"fo\"     , 2) == true\n   * streqabbr(\"foo\", \"fo\"     , 0) == false\n   * streqabbr(\"foo\", \"f=bar\"  , 2) == false\n   * streqabbr(\"foo\", \"f\"      , 2) == false\n   * streqabbr(\"fo\" , \"foo=bar\",<anything>)  == false\n   * streqabbr(\"foo\", \"foobar\" ,<anything>)  == false\n   * streqabbr(\"foo\", \"fobar\"  ,<anything>)  == false\n   * streqabbr(\"foo\", \"foo\"    ,<anything>)  == true\n   * @endcode\n   */\n  static bool streqabbr(const char* st1, const char* st2, long long min)\n  {\n    const char* st1start = st1;\n    while (*st1 != 0 && (*st1 == *st2))\n    {\n      ++st1;\n      ++st2;\n    }\n\n    return (*st1 == 0 || (min > 0 && (st1 - st1start) >= min)) && (*st2 == 0 || *st2 == '=');\n  }\n\n  /**\n   * @internal\n   * @brief Returns true iff character @c ch is contained in the string @c st.\n   *\n   * Returns @c true for @c ch==0 .\n   */\n  static bool instr(char ch, const char* st)\n  {\n    while (*st != 0 && *st != ch)\n      ++st;\n    return *st == ch;\n  }\n\n  /**\n   * @internal\n   * @brief Rotates <code>args[-count],...,args[-1],args[0]</code> to become\n   *        <code>args[0],args[-count],...,args[-1]</code>.\n   */\n  static void shift(const char** args, int count)\n  {\n    for (int i = 0; i > -count; --i)\n    {\n      const char* temp = args[i];\n      args[i] = args[i - 1];\n      args[i - 1] = temp;\n    }\n  }\n};\n\n/**\n * @internal\n * @brief Interface for actions Parser::workhorse() should perform for each Option it\n * parses.\n */\nstruct Parser::Action\n{\n  /**\n   * @brief Called by Parser::workhorse() for each Option that has been successfully\n   * parsed (including unknown\n   * options if they have a Descriptor whose Descriptor::check_arg does not return\n   * @ref ARG_ILLEGAL.\n   *\n   * Returns @c false iff a fatal error has occured and the parse should be aborted.\n   */\n  virtual bool perform(Option&)\n  {\n    return true;\n  }\n\n  /**\n   * @brief Called by Parser::workhorse() after finishing the parse.\n   * @param numargs the number of non-option arguments remaining\n   * @param args pointer to the first remaining non-option argument (if numargs > 0).\n   *\n   * @return\n   * @c false iff a fatal error has occurred.\n   */\n  virtual bool finished(int numargs, const char** args)\n  {\n    (void) numargs;\n    (void) args;\n    return true;\n  }\n};\n\n/**\n * @internal\n * @brief An Action to pass to Parser::workhorse() that will increment a counter for\n * each parsed Option.\n */\nclass Stats::CountOptionsAction: public Parser::Action\n{\n  unsigned* buffer_max;\npublic:\n  /**\n   * Creates a new CountOptionsAction that will increase @c *buffer_max_ for each\n   * parsed Option.\n   */\n  CountOptionsAction(unsigned* buffer_max_) :\n      buffer_max(buffer_max_)\n  {\n  }\n\n  bool perform(Option&)\n  {\n    if (*buffer_max == 0x7fffffff)\n      return false; // overflow protection: don't accept number of options that doesn't fit signed int\n    ++*buffer_max;\n    return true;\n  }\n};\n\n/**\n * @internal\n * @brief An Action to pass to Parser::workhorse() that will store each parsed Option in\n * appropriate arrays (see Parser::parse()).\n */\nclass Parser::StoreOptionAction: public Parser::Action\n{\n  Parser& parser;\n  Option* options;\n  Option* buffer;\n  int bufmax; //! Number of slots in @c buffer. @c -1 means \"large enough\".\npublic:\n  /**\n   * @brief Creates a new StoreOption action.\n   * @param parser_ the parser whose op_count should be updated.\n   * @param options_ each Option @c o is chained into the linked list @c options_[o.desc->index]\n   * @param buffer_ each Option is appended to this array as long as there's a free slot.\n   * @param bufmax_ number of slots in @c buffer_. @c -1 means \"large enough\".\n   */\n  StoreOptionAction(Parser& parser_, Option options_[], Option buffer_[], int bufmax_) :\n      parser(parser_), options(options_), buffer(buffer_), bufmax(bufmax_)\n  {\n    // find first empty slot in buffer (if any)\n    int bufidx = 0;\n    while ((bufmax < 0 || bufidx < bufmax) && buffer[bufidx])\n      ++bufidx;\n\n    // set parser's optionCount\n    parser.op_count = bufidx;\n  }\n\n  bool perform(Option& option)\n  {\n    if (bufmax < 0 || parser.op_count < bufmax)\n    {\n      if (parser.op_count == 0x7fffffff)\n        return false; // overflow protection: don't accept number of options that doesn't fit signed int\n\n      buffer[parser.op_count] = option;\n      int idx = buffer[parser.op_count].desc->index;\n      if (options[idx])\n        options[idx].append(buffer[parser.op_count]);\n      else\n        options[idx] = buffer[parser.op_count];\n      ++parser.op_count;\n    }\n    return true; // NOTE: an option that is discarded because of a full buffer is not fatal\n  }\n\n  bool finished(int numargs, const char** args)\n  {\n    // only overwrite non-option argument list if there's at least 1\n    // new non-option argument. Otherwise we keep the old list. This\n    // makes it easy to use default non-option arguments.\n    if (numargs > 0)\n    {\n      parser.nonop_count = numargs;\n      parser.nonop_args = args;\n    }\n\n    return true;\n  }\n};\n\ninline void Parser::parse(bool gnu, const Descriptor usage[], int argc, const char** argv, Option options[],\n                          Option buffer[], int min_abbr_len, bool single_minus_longopt, int bufmax)\n{\n  StoreOptionAction action(*this, options, buffer, bufmax);\n  err = !workhorse(gnu, usage, argc, argv, action, single_minus_longopt, true, min_abbr_len);\n}\n\ninline void Stats::add(bool gnu, const Descriptor usage[], int argc, const char** argv, int min_abbr_len,\n                       bool single_minus_longopt)\n{\n  // determine size of options array. This is the greatest index used in the usage + 1\n  int i = 0;\n  while (usage[i].shortopt != 0)\n  {\n    if (usage[i].index + 1 >= options_max)\n      options_max = (usage[i].index + 1) + 1; // 1 more than necessary as sentinel\n\n    ++i;\n  }\n\n  CountOptionsAction action(&buffer_max);\n  Parser::workhorse(gnu, usage, argc, argv, action, single_minus_longopt, false, min_abbr_len);\n}\n\ninline bool Parser::workhorse(bool gnu, const Descriptor usage[], int numargs, const char** args, Action& action,\n                              bool single_minus_longopt, bool print_errors, int min_abbr_len)\n{\n  // protect against NULL pointer\n  if (args == 0)\n    numargs = 0;\n\n  int nonops = 0;\n\n  while (numargs != 0 && *args != 0)\n  {\n    const char* param = *args; // param can be --long-option, -srto or non-option argument\n\n    // in POSIX mode the first non-option argument terminates the option list\n    // a lone minus character is a non-option argument\n    if (param[0] != '-' || param[1] == 0)\n    {\n      if (gnu)\n      {\n        ++nonops;\n        ++args;\n        if (numargs > 0)\n          --numargs;\n        continue;\n      }\n      else\n        break;\n    }\n\n    // -- terminates the option list. The -- itself is skipped.\n    if (param[1] == '-' && param[2] == 0)\n    {\n      shift(args, nonops);\n      ++args;\n      if (numargs > 0)\n        --numargs;\n      break;\n    }\n\n    bool handle_short_options;\n    const char* longopt_name;\n    if (param[1] == '-') // if --long-option\n    {\n      handle_short_options = false;\n      longopt_name = param + 2;\n    }\n    else\n    {\n      handle_short_options = true;\n      longopt_name = param + 1; //for testing a potential -long-option\n    }\n\n    bool try_single_minus_longopt = single_minus_longopt;\n    bool have_more_args = (numargs > 1 || numargs < 0); // is referencing argv[1] valid?\n\n    do // loop over short options in group, for long options the body is executed only once\n    {\n      int idx = 0;\n\n      const char* optarg = 0;\n\n      /******************** long option **********************/\n      if (handle_short_options == false || try_single_minus_longopt)\n      {\n        idx = 0;\n        while (usage[idx].longopt != 0 && !streq(usage[idx].longopt, longopt_name))\n          ++idx;\n\n        if (usage[idx].longopt == 0 && min_abbr_len > 0) // if we should try to match abbreviated long options\n        {\n          int i1 = 0;\n          while (usage[i1].longopt != 0 && !streqabbr(usage[i1].longopt, longopt_name, min_abbr_len))\n            ++i1;\n          if (usage[i1].longopt != 0)\n          { // now test if the match is unambiguous by checking for another match\n            int i2 = i1 + 1;\n            while (usage[i2].longopt != 0 && !streqabbr(usage[i2].longopt, longopt_name, min_abbr_len))\n              ++i2;\n\n            if (usage[i2].longopt == 0) // if there was no second match it's unambiguous, so accept i1 as idx\n              idx = i1;\n          }\n        }\n\n        // if we found something, disable handle_short_options (only relevant if single_minus_longopt)\n        if (usage[idx].longopt != 0)\n          handle_short_options = false;\n\n        try_single_minus_longopt = false; // prevent looking for longopt in the middle of shortopt group\n\n        optarg = longopt_name;\n        while (*optarg != 0 && *optarg != '=')\n          ++optarg;\n        if (*optarg == '=') // attached argument\n          ++optarg;\n        else\n          // possibly detached argument\n          optarg = (have_more_args ? args[1] : 0);\n      }\n\n      /************************ short option ***********************************/\n      if (handle_short_options)\n      {\n        if (*++param == 0) // point at the 1st/next option character\n          break; // end of short option group\n\n        idx = 0;\n        while (usage[idx].shortopt != 0 && !instr(*param, usage[idx].shortopt))\n          ++idx;\n\n        if (param[1] == 0) // if the potential argument is separate\n          optarg = (have_more_args ? args[1] : 0);\n        else\n          // if the potential argument is attached\n          optarg = param + 1;\n      }\n\n      const Descriptor* descriptor = &usage[idx];\n\n      if (descriptor->shortopt == 0) /**************  unknown option ********************/\n      {\n        // look for dummy entry (shortopt == \"\" and longopt == \"\") to use as Descriptor for unknown options\n        idx = 0;\n        while (usage[idx].shortopt != 0 && (usage[idx].shortopt[0] != 0 || usage[idx].longopt[0] != 0))\n          ++idx;\n        descriptor = (usage[idx].shortopt == 0 ? 0 : &usage[idx]);\n      }\n\n      if (descriptor != 0)\n      {\n        Option option(descriptor, param, optarg);\n        switch (descriptor->check_arg(option, print_errors))\n        {\n          case ARG_ILLEGAL:\n            return false; // fatal\n          case ARG_OK:\n            // skip one element of the argument vector, if it's a separated argument\n            if (optarg != 0 && have_more_args && optarg == args[1])\n            {\n              shift(args, nonops);\n              if (numargs > 0)\n                --numargs;\n              ++args;\n            }\n\n            // No further short options are possible after an argument\n            handle_short_options = false;\n\n            break;\n          case ARG_IGNORE:\n          case ARG_NONE:\n            option.arg = 0;\n            break;\n        }\n\n        if (!action.perform(option))\n          return false;\n      }\n\n    } while (handle_short_options);\n\n    shift(args, nonops);\n    ++args;\n    if (numargs > 0)\n      --numargs;\n\n  } // while\n\n  if (numargs > 0 && *args == 0) // It's a bug in the caller if numargs is greater than the actual number\n    numargs = 0; // of arguments, but as a service to the user we fix this if we spot it.\n\n  if (numargs < 0) // if we don't know the number of remaining non-option arguments\n  { // we need to count them\n    numargs = 0;\n    while (args[numargs] != 0)\n      ++numargs;\n  }\n\n  return action.finished(numargs + nonops, args - nonops);\n}\n\n/**\n * @internal\n * @brief The implementation of option::printUsage().\n */\nstruct PrintUsageImplementation\n{\n  /**\n   * @internal\n   * @brief Interface for Functors that write (part of) a string somewhere.\n   */\n  struct IStringWriter\n  {\n    /**\n     * @brief Writes the given number of chars beginning at the given pointer somewhere.\n     */\n    virtual void operator()(const char*, int)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Encapsulates a function with signature <code>func(string, size)</code> where\n   * string can be initialized with a const char* and size with an int.\n   */\n  template<typename Function>\n  struct FunctionWriter: public IStringWriter\n  {\n    Function* write;\n\n    virtual void operator()(const char* str, int size)\n    {\n      (*write)(str, size);\n    }\n\n    FunctionWriter(Function* w) :\n        write(w)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Encapsulates a reference to an object with a <code>write(string, size)</code>\n   * method like that of @c std::ostream.\n   */\n  template<typename OStream>\n  struct OStreamWriter: public IStringWriter\n  {\n    OStream& ostream;\n\n    virtual void operator()(const char* str, int size)\n    {\n      ostream.write(str, size);\n    }\n\n    OStreamWriter(OStream& o) :\n        ostream(o)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Like OStreamWriter but encapsulates a @c const reference, which is\n   * typically a temporary object of a user class.\n   */\n  template<typename Temporary>\n  struct TemporaryWriter: public IStringWriter\n  {\n    const Temporary& userstream;\n\n    virtual void operator()(const char* str, int size)\n    {\n      userstream.write(str, size);\n    }\n\n    TemporaryWriter(const Temporary& u) :\n        userstream(u)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Encapsulates a function with the signature <code>func(fd, string, size)</code> (the\n   * signature of the @c write() system call)\n   * where fd can be initialized from an int, string from a const char* and size from an int.\n   */\n  template<typename Syscall>\n  struct SyscallWriter: public IStringWriter\n  {\n    Syscall* write;\n    int fd;\n\n    virtual void operator()(const char* str, int size)\n    {\n      (*write)(fd, str, size);\n    }\n\n    SyscallWriter(Syscall* w, int f) :\n        write(w), fd(f)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Encapsulates a function with the same signature as @c std::fwrite().\n   */\n  template<typename Function, typename Stream>\n  struct StreamWriter: public IStringWriter\n  {\n    Function* fwrite;\n    Stream* stream;\n\n    virtual void operator()(const char* str, int size)\n    {\n      (*fwrite)(str, size, 1, stream);\n    }\n\n    StreamWriter(Function* w, Stream* s) :\n        fwrite(w), stream(s)\n    {\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Sets <code> i1 = max(i1, i2) </code>\n   */\n  static void upmax(int& i1, int i2)\n  {\n    i1 = (i1 >= i2 ? i1 : i2);\n  }\n\n  /**\n   * @internal\n   * @brief Moves the \"cursor\" to column @c want_x assuming it is currently at column @c x\n   * and sets @c x=want_x .\n   * If <code> x > want_x </code>, a line break is output before indenting.\n   *\n   * @param write Spaces and possibly a line break are written via this functor to get\n   *        the desired indentation @c want_x .\n   * @param[in,out] x the current indentation. Set to @c want_x by this method.\n   * @param want_x the desired indentation.\n   */\n  static void indent(IStringWriter& write, int& x, int want_x)\n  {\n    int indent = want_x - x;\n    if (indent < 0)\n    {\n      write(\"\\n\", 1);\n      indent = want_x;\n    }\n\n    if (indent > 0)\n    {\n      char space = ' ';\n      for (int i = 0; i < indent; ++i)\n        write(&space, 1);\n      x = want_x;\n    }\n  }\n\n  /**\n   * @brief Returns true if ch is the unicode code point of a wide character.\n   *\n   * @note\n   * The following character ranges are treated as wide\n   * @code\n   * 1100..115F\n   * 2329..232A  (just 2 characters!)\n   * 2E80..A4C6  except for 303F\n   * A960..A97C\n   * AC00..D7FB\n   * F900..FAFF\n   * FE10..FE6B\n   * FF01..FF60\n   * FFE0..FFE6\n   * 1B000......\n   * @endcode\n   */\n  static bool isWideChar(unsigned ch)\n  {\n    if (ch == 0x303F)\n      return false;\n\n    return ((0x1100 <= ch && ch <= 0x115F) || (0x2329 <= ch && ch <= 0x232A) || (0x2E80 <= ch && ch <= 0xA4C6)\n        || (0xA960 <= ch && ch <= 0xA97C) || (0xAC00 <= ch && ch <= 0xD7FB) || (0xF900 <= ch && ch <= 0xFAFF)\n        || (0xFE10 <= ch && ch <= 0xFE6B) || (0xFF01 <= ch && ch <= 0xFF60) || (0xFFE0 <= ch && ch <= 0xFFE6)\n        || (0x1B000 <= ch));\n  }\n\n  /**\n   * @internal\n   * @brief Splits a @c Descriptor[] array into tables, rows, lines and columns and\n   * iterates over these components.\n   *\n   * The top-level organizational unit is the @e table.\n   * A table begins at a Descriptor with @c help!=NULL and extends up to\n   * a Descriptor with @c help==NULL.\n   *\n   * A table consists of @e rows. Due to line-wrapping and explicit breaks\n   * a row may take multiple lines on screen. Rows within the table are separated\n   * by \\\\n. They never cross Descriptor boundaries. This means a row ends either\n   * at \\\\n or the 0 at the end of the help string.\n   *\n   * A row consists of columns/cells. Columns/cells within a row are separated by \\\\t.\n   * Line breaks within a cell are marked by \\\\v.\n   *\n   * Rows in the same table need not have the same number of columns/cells. The\n   * extreme case are interjections, which are rows that contain neither \\\\t nor \\\\v.\n   * These are NOT treated specially by LinePartIterator, but they are treated\n   * specially by printUsage().\n   *\n   * LinePartIterator iterates through the usage at 3 levels: table, row and part.\n   * Tables and rows are as described above. A @e part is a line within a cell.\n   * LinePartIterator iterates through 1st parts of all cells, then through the 2nd\n   * parts of all cells (if any),... @n\n   * Example: The row <code> \"1 \\v 3 \\t 2 \\v 4\" </code> has 2 cells/columns and 4 parts.\n   * The parts will be returned in the order 1, 2, 3, 4.\n   *\n   * It is possible that some cells have fewer parts than others. In this case\n   * LinePartIterator will \"fill up\" these cells with 0-length parts. IOW, LinePartIterator\n   * always returns the same number of parts for each column. Note that this is different\n   * from the way rows and columns are handled. LinePartIterator does @e not guarantee that\n   * the same number of columns will be returned for each row.\n   *\n   */\n  class LinePartIterator\n  {\n    const Descriptor* tablestart; //!< The 1st descriptor of the current table.\n    const Descriptor* rowdesc; //!< The Descriptor that contains the current row.\n    const char* rowstart; //!< Ptr to 1st character of current row within rowdesc->help.\n    const char* ptr; //!< Ptr to current part within the current row.\n    int col; //!< Index of current column.\n    int len; //!< Length of the current part (that ptr points at) in BYTES\n    int screenlen; //!< Length of the current part in screen columns (taking narrow/wide chars into account).\n    int max_line_in_block; //!< Greatest index of a line within the block. This is the number of \\\\v within the cell with the most \\\\vs.\n    int line_in_block; //!< Line index within the current cell of the current part.\n    int target_line_in_block; //!< Line index of the parts we should return to the user on this iteration.\n    bool hit_target_line; //!< Flag whether we encountered a part with line index target_line_in_block in the current cell.\n\n    /**\n     * @brief Determines the byte and character lengths of the part at @ref ptr and\n     * stores them in @ref len and @ref screenlen respectively.\n     */\n    void update_length()\n    {\n      screenlen = 0;\n      for (len = 0; ptr[len] != 0 && ptr[len] != '\\v' && ptr[len] != '\\t' && ptr[len] != '\\n'; ++len)\n      {\n        ++screenlen;\n        unsigned ch = (unsigned char) ptr[len];\n        if (ch > 0xC1) // everything <= 0xC1 (yes, even 0xC1 itself) is not a valid UTF-8 start byte\n        {\n          // int __builtin_clz (unsigned int x)\n          // Returns the number of leading 0-bits in x, starting at the most significant bit\n          unsigned mask = (unsigned) -1 >> __builtin_clz(ch ^ 0xff);\n          ch = ch & mask; // mask out length bits, we don't verify their correctness\n          while (((unsigned char) ptr[len + 1] ^ 0x80) <= 0x3F) // while next byte is continuation byte\n          {\n            ch = (ch << 6) ^ (unsigned char) ptr[len + 1] ^ 0x80; // add continuation to char code\n            ++len;\n          }\n          // ch is the decoded unicode code point\n          if (ch >= 0x1100 && isWideChar(ch)) // the test for 0x1100 is here to avoid the function call in the Latin case\n            ++screenlen;\n        }\n      }\n    }\n\n  public:\n    //! @brief Creates an iterator for @c usage.\n    LinePartIterator(const Descriptor usage[]) :\n        tablestart(usage), rowdesc(0), rowstart(0), ptr(0), col(-1), len(0), max_line_in_block(0), line_in_block(0),\n        target_line_in_block(0), hit_target_line(true)\n    {\n    }\n\n    /**\n     * @brief Moves iteration to the next table (if any). Has to be called once on a new\n     * LinePartIterator to move to the 1st table.\n     * @retval false if moving to next table failed because no further table exists.\n     */\n    bool nextTable()\n    {\n      // If this is NOT the first time nextTable() is called after the constructor,\n      // then skip to the next table break (i.e. a Descriptor with help == 0)\n      if (rowdesc != 0)\n      {\n        while (tablestart->help != 0 && tablestart->shortopt != 0)\n          ++tablestart;\n      }\n\n      // Find the next table after the break (if any)\n      while (tablestart->help == 0 && tablestart->shortopt != 0)\n        ++tablestart;\n\n      restartTable();\n      return rowstart != 0;\n    }\n\n    /**\n     * @brief Reset iteration to the beginning of the current table.\n     */\n    void restartTable()\n    {\n      rowdesc = tablestart;\n      rowstart = tablestart->help;\n      ptr = 0;\n    }\n\n    /**\n     * @brief Moves iteration to the next row (if any). Has to be called once after each call to\n     * @ref nextTable() to move to the 1st row of the table.\n     * @retval false if moving to next row failed because no further row exists.\n     */\n    bool nextRow()\n    {\n      if (ptr == 0)\n      {\n        restartRow();\n        return rowstart != 0;\n      }\n\n      while (*ptr != 0 && *ptr != '\\n')\n        ++ptr;\n\n      if (*ptr == 0)\n      {\n        if ((rowdesc + 1)->help == 0) // table break\n          return false;\n\n        ++rowdesc;\n        rowstart = rowdesc->help;\n      }\n      else // if (*ptr == '\\n')\n      {\n        rowstart = ptr + 1;\n      }\n\n      restartRow();\n      return true;\n    }\n\n    /**\n     * @brief Reset iteration to the beginning of the current row.\n     */\n    void restartRow()\n    {\n      ptr = rowstart;\n      col = -1;\n      len = 0;\n      screenlen = 0;\n      max_line_in_block = 0;\n      line_in_block = 0;\n      target_line_in_block = 0;\n      hit_target_line = true;\n    }\n\n    /**\n     * @brief Moves iteration to the next part (if any). Has to be called once after each call to\n     * @ref nextRow() to move to the 1st part of the row.\n     * @retval false if moving to next part failed because no further part exists.\n     *\n     * See @ref LinePartIterator for details about the iteration.\n     */\n    bool next()\n    {\n      if (ptr == 0)\n        return false;\n\n      if (col == -1)\n      {\n        col = 0;\n        update_length();\n        return true;\n      }\n\n      ptr += len;\n      while (true)\n      {\n        switch (*ptr)\n        {\n          case '\\v':\n            upmax(max_line_in_block, ++line_in_block);\n            ++ptr;\n            break;\n          case '\\t':\n            if (!hit_target_line) // if previous column did not have the targetline\n            { // then \"insert\" a 0-length part\n              update_length();\n              hit_target_line = true;\n              return true;\n            }\n\n            hit_target_line = false;\n            line_in_block = 0;\n            ++col;\n            ++ptr;\n            break;\n          case 0:\n          case '\\n':\n            if (!hit_target_line) // if previous column did not have the targetline\n            { // then \"insert\" a 0-length part\n              update_length();\n              hit_target_line = true;\n              return true;\n            }\n\n            if (++target_line_in_block > max_line_in_block)\n            {\n              update_length();\n              return false;\n            }\n\n            hit_target_line = false;\n            line_in_block = 0;\n            col = 0;\n            ptr = rowstart;\n            continue;\n          default:\n            ++ptr;\n            continue;\n        } // switch\n\n        if (line_in_block == target_line_in_block)\n        {\n          update_length();\n          hit_target_line = true;\n          return true;\n        }\n      } // while\n    }\n\n    /**\n     * @brief Returns the index (counting from 0) of the column in which\n     * the part pointed to by @ref data() is located.\n     */\n    int column()\n    {\n      return col;\n    }\n\n    /**\n     * @brief Returns the index (counting from 0) of the line within the current column\n     * this part belongs to.\n     */\n    int line()\n    {\n      return target_line_in_block; // NOT line_in_block !!! It would be wrong if !hit_target_line\n    }\n\n    /**\n     * @brief Returns the length of the part pointed to by @ref data() in raw chars (not UTF-8 characters).\n     */\n    int length()\n    {\n      return len;\n    }\n\n    /**\n     * @brief Returns the width in screen columns of the part pointed to by @ref data().\n     * Takes multi-byte UTF-8 sequences and wide characters into account.\n     */\n    int screenLength()\n    {\n      return screenlen;\n    }\n\n    /**\n     * @brief Returns the current part of the iteration.\n     */\n    const char* data()\n    {\n      return ptr;\n    }\n  };\n\n  /**\n   * @internal\n   * @brief Takes input and line wraps it, writing out one line at a time so that\n   * it can be interleaved with output from other columns.\n   *\n   * The LineWrapper is used to handle the last column of each table as well as interjections.\n   * The LineWrapper is called once for each line of output. If the data given to it fits\n   * into the designated width of the last column it is simply written out. If there\n   * is too much data, an appropriate split point is located and only the data up to this\n   * split point is written out. The rest of the data is queued for the next line.\n   * That way the last column can be line wrapped and interleaved with data from\n   * other columns. The following example makes this clearer:\n   * @code\n   * Column 1,1    Column 2,1     This is a long text\n   * Column 1,2    Column 2,2     that does not fit into\n   *                              a single line.\n   * @endcode\n   *\n   * The difficulty in producing this output is that the whole string\n   * \"This is a long text that does not fit into a single line\" is the\n   * 1st and only part of column 3. In order to produce the above\n   * output the string must be output piecemeal, interleaved with\n   * the data from the other columns.\n   */\n  class LineWrapper\n  {\n    static const int bufmask = 15; //!< Must be a power of 2 minus 1.\n    /**\n     * @brief Ring buffer for length component of pair (data, length).\n     */\n    int lenbuf[bufmask + 1];\n    /**\n     * @brief Ring buffer for data component of pair (data, length).\n     */\n    const char* datbuf[bufmask + 1];\n    /**\n     * @brief The indentation of the column to which the LineBuffer outputs. LineBuffer\n     * assumes that the indentation has already been written when @ref process()\n     * is called, so this value is only used when a buffer flush requires writing\n     * additional lines of output.\n     */\n    int x;\n    /**\n     * @brief The width of the column to line wrap.\n     */\n    int width;\n    int head; //!< @brief index for next write\n    int tail; //!< @brief index for next read - 1 (i.e. increment tail BEFORE read)\n\n    /**\n     * @brief Multiple methods of LineWrapper may decide to flush part of the buffer to\n     * free up space. The contract of process() says that only 1 line is output. So\n     * this variable is used to track whether something has output a line. It is\n     * reset at the beginning of process() and checked at the end to decide if\n     * output has already occurred or is still needed.\n     */\n    bool wrote_something;\n\n    bool buf_empty()\n    {\n      return ((tail + 1) & bufmask) == head;\n    }\n\n    bool buf_full()\n    {\n      return tail == head;\n    }\n\n    void buf_store(const char* data, int len)\n    {\n      lenbuf[head] = len;\n      datbuf[head] = data;\n      head = (head + 1) & bufmask;\n    }\n\n    //! @brief Call BEFORE reading ...buf[tail].\n    void buf_next()\n    {\n      tail = (tail + 1) & bufmask;\n    }\n\n    /**\n     * @brief Writes (data,len) into the ring buffer. If the buffer is full, a single line\n     * is flushed out of the buffer into @c write.\n     */\n    void output(IStringWriter& write, const char* data, int len)\n    {\n      if (buf_full())\n        write_one_line(write);\n\n      buf_store(data, len);\n    }\n\n    /**\n     * @brief Writes a single line of output from the buffer to @c write.\n     */\n    void write_one_line(IStringWriter& write)\n    {\n      if (wrote_something) // if we already wrote something, we need to start a new line\n      {\n        write(\"\\n\", 1);\n        int _ = 0;\n        indent(write, _, x);\n      }\n\n      if (!buf_empty())\n      {\n        buf_next();\n        write(datbuf[tail], lenbuf[tail]);\n      }\n\n      wrote_something = true;\n    }\n  public:\n\n    /**\n     * @brief Writes out all remaining data from the LineWrapper using @c write.\n     * Unlike @ref process() this method indents all lines including the first and\n     * will output a \\\\n at the end (but only if something has been written).\n     */\n    void flush(IStringWriter& write)\n    {\n      if (buf_empty())\n        return;\n      int _ = 0;\n      indent(write, _, x);\n      wrote_something = false;\n      while (!buf_empty())\n        write_one_line(write);\n      write(\"\\n\", 1);\n    }\n\n    /**\n     * @brief Process, wrap and output the next piece of data.\n     *\n     * process() will output at least one line of output. This is not necessarily\n     * the @c data passed in. It may be data queued from a prior call to process().\n     * If the internal buffer is full, more than 1 line will be output.\n     *\n     * process() assumes that the a proper amount of indentation has already been\n     * output. It won't write any further indentation before the 1st line. If\n     * more than 1 line is written due to buffer constraints, the lines following\n     * the first will be indented by this method, though.\n     *\n     * No \\\\n is written by this method after the last line that is written.\n     *\n     * @param write where to write the data.\n     * @param data the new chunk of data to write.\n     * @param len the length of the chunk of data to write.\n     */\n    void process(IStringWriter& write, const char* data, int len)\n    {\n      wrote_something = false;\n\n      while (len > 0)\n      {\n        if (len <= width) // quick test that works because utf8width <= len (all wide chars have at least 2 bytes)\n        {\n          output(write, data, len);\n          len = 0;\n        }\n        else // if (len > width)  it's possible (but not guaranteed) that utf8len > width\n        {\n          int utf8width = 0;\n          int maxi = 0;\n          while (maxi < len && utf8width < width)\n          {\n            int charbytes = 1;\n            unsigned ch = (unsigned char) data[maxi];\n            if (ch > 0xC1) // everything <= 0xC1 (yes, even 0xC1 itself) is not a valid UTF-8 start byte\n            {\n              // int __builtin_clz (unsigned int x)\n              // Returns the number of leading 0-bits in x, starting at the most significant bit\n              unsigned mask = (unsigned) -1 >> __builtin_clz(ch ^ 0xff);\n              ch = ch & mask; // mask out length bits, we don't verify their correctness\n              while ((maxi + charbytes < len) && //\n                  (((unsigned char) data[maxi + charbytes] ^ 0x80) <= 0x3F)) // while next byte is continuation byte\n              {\n                ch = (ch << 6) ^ (unsigned char) data[maxi + charbytes] ^ 0x80; // add continuation to char code\n                ++charbytes;\n              }\n              // ch is the decoded unicode code point\n              if (ch >= 0x1100 && isWideChar(ch)) // the test for 0x1100 is here to avoid the function call in the Latin case\n              {\n                if (utf8width + 2 > width)\n                  break;\n                ++utf8width;\n              }\n            }\n            ++utf8width;\n            maxi += charbytes;\n          }\n\n          // data[maxi-1] is the last byte of the UTF-8 sequence of the last character that fits\n          // onto the 1st line. If maxi == len, all characters fit on the line.\n\n          if (maxi == len)\n          {\n            output(write, data, len);\n            len = 0;\n          }\n          else // if (maxi < len)  at least 1 character (data[maxi] that is) doesn't fit on the line\n          {\n            int i;\n            for (i = maxi; i >= 0; --i)\n              if (data[i] == ' ')\n                break;\n\n            if (i >= 0)\n            {\n              output(write, data, i);\n              data += i + 1;\n              len -= i + 1;\n            }\n            else // did not find a space to split at => split before data[maxi]\n            { // data[maxi] is always the beginning of a character, never a continuation byte\n              output(write, data, maxi);\n              data += maxi;\n              len -= maxi;\n            }\n          }\n        }\n      }\n      if (!wrote_something) // if we didn't already write something to make space in the buffer\n        write_one_line(write); // write at most one line of actual output\n    }\n\n    /**\n     * @brief Constructs a LineWrapper that wraps its output to fit into\n     * screen columns @c x1 (incl.) to @c x2 (excl.).\n     *\n     * @c x1 gives the indentation LineWrapper uses if it needs to indent.\n     */\n    LineWrapper(int x1, int x2) :\n        x(x1), width(x2 - x1), head(0), tail(bufmask)\n    {\n      if (width < 2) // because of wide characters we need at least width 2 or the code breaks\n        width = 2;\n    }\n  };\n\n  /**\n   * @internal\n   * @brief This is the implementation that is shared between all printUsage() templates.\n   * Because all printUsage() templates share this implementation, there is no template bloat.\n   */\n  static void printUsage(IStringWriter& write, const Descriptor usage[], int width = 80, //\n                         int last_column_min_percent = 50, int last_column_own_line_max_percent = 75)\n  {\n    if (width < 1) // protect against nonsense values\n      width = 80;\n\n    if (width > 10000) // protect against overflow in the following computation\n      width = 10000;\n\n    int last_column_min_width = ((width * last_column_min_percent) + 50) / 100;\n    int last_column_own_line_max_width = ((width * last_column_own_line_max_percent) + 50) / 100;\n    if (last_column_own_line_max_width == 0)\n      last_column_own_line_max_width = 1;\n\n    LinePartIterator part(usage);\n    while (part.nextTable())\n    {\n\n      /***************** Determine column widths *******************************/\n\n      const int maxcolumns = 8; // 8 columns are enough for everyone\n      int col_width[maxcolumns];\n      int lastcolumn;\n      int leftwidth;\n      int overlong_column_threshold = 10000;\n      do\n      {\n        lastcolumn = 0;\n        for (int i = 0; i < maxcolumns; ++i)\n          col_width[i] = 0;\n\n        part.restartTable();\n        while (part.nextRow())\n        {\n          while (part.next())\n          {\n            if (part.column() < maxcolumns)\n            {\n              upmax(lastcolumn, part.column());\n              if (part.screenLength() < overlong_column_threshold)\n                // We don't let rows that don't use table separators (\\t or \\v) influence\n                // the width of column 0. This allows the user to interject section headers\n                // or explanatory paragraphs that do not participate in the table layout.\n                if (part.column() > 0 || part.line() > 0 || part.data()[part.length()] == '\\t'\n                    || part.data()[part.length()] == '\\v')\n                  upmax(col_width[part.column()], part.screenLength());\n            }\n          }\n        }\n\n        /*\n         * If the last column doesn't fit on the same\n         * line as the other columns, we can fix that by starting it on its own line.\n         * However we can't do this for any of the columns 0..lastcolumn-1.\n         * If their sum exceeds the maximum width we try to fix this by iteratively\n         * ignoring the widest line parts in the width determination until\n         * we arrive at a series of column widths that fit into one line.\n         * The result is a layout where everything is nicely formatted\n         * except for a few overlong fragments.\n         * */\n\n        leftwidth = 0;\n        overlong_column_threshold = 0;\n        for (int i = 0; i < lastcolumn; ++i)\n        {\n          leftwidth += col_width[i];\n          upmax(overlong_column_threshold, col_width[i]);\n        }\n\n      } while (leftwidth > width);\n\n      /**************** Determine tab stops and last column handling **********************/\n\n      int tabstop[maxcolumns];\n      tabstop[0] = 0;\n      for (int i = 1; i < maxcolumns; ++i)\n        tabstop[i] = tabstop[i - 1] + col_width[i - 1];\n\n      int rightwidth = width - tabstop[lastcolumn];\n      bool print_last_column_on_own_line = false;\n      if (rightwidth < last_column_min_width &&  // if we don't have the minimum requested width for the last column\n            ( col_width[lastcolumn] == 0 ||      // and all last columns are > overlong_column_threshold\n              rightwidth < col_width[lastcolumn] // or there is at least one last column that requires more than the space available\n            )\n          )\n      {\n        print_last_column_on_own_line = true;\n        rightwidth = last_column_own_line_max_width;\n      }\n\n      // If lastcolumn == 0 we must disable print_last_column_on_own_line because\n      // otherwise 2 copies of the last (and only) column would be output.\n      // Actually this is just defensive programming. It is currently not\n      // possible that lastcolumn==0 and print_last_column_on_own_line==true\n      // at the same time, because lastcolumn==0 => tabstop[lastcolumn] == 0 =>\n      // rightwidth==width => rightwidth>=last_column_min_width  (unless someone passes\n      // a bullshit value >100 for last_column_min_percent) => the above if condition\n      // is false => print_last_column_on_own_line==false\n      if (lastcolumn == 0)\n        print_last_column_on_own_line = false;\n\n      LineWrapper lastColumnLineWrapper(width - rightwidth, width);\n      LineWrapper interjectionLineWrapper(0, width);\n\n      part.restartTable();\n\n      /***************** Print out all rows of the table *************************************/\n\n      while (part.nextRow())\n      {\n        int x = -1;\n        while (part.next())\n        {\n          if (part.column() > lastcolumn)\n            continue; // drop excess columns (can happen if lastcolumn == maxcolumns-1)\n\n          if (part.column() == 0)\n          {\n            if (x >= 0)\n              write(\"\\n\", 1);\n            x = 0;\n          }\n\n          indent(write, x, tabstop[part.column()]);\n\n          if ((part.column() < lastcolumn)\n              && (part.column() > 0 || part.line() > 0 || part.data()[part.length()] == '\\t'\n                  || part.data()[part.length()] == '\\v'))\n          {\n            write(part.data(), part.length());\n            x += part.screenLength();\n          }\n          else // either part.column() == lastcolumn or we are in the special case of\n               // an interjection that doesn't contain \\v or \\t\n          {\n            // NOTE: This code block is not necessarily executed for\n            // each line, because some rows may have fewer columns.\n\n            LineWrapper& lineWrapper = (part.column() == 0) ? interjectionLineWrapper : lastColumnLineWrapper;\n\n            if (!print_last_column_on_own_line || part.column() != lastcolumn)\n              lineWrapper.process(write, part.data(), part.length());\n          }\n        } // while\n\n        if (print_last_column_on_own_line)\n        {\n          part.restartRow();\n          while (part.next())\n          {\n            if (part.column() == lastcolumn)\n            {\n              write(\"\\n\", 1);\n              int _ = 0;\n              indent(write, _, width - rightwidth);\n              lastColumnLineWrapper.process(write, part.data(), part.length());\n            }\n          }\n        }\n\n        write(\"\\n\", 1);\n        lastColumnLineWrapper.flush(write);\n        interjectionLineWrapper.flush(write);\n      }\n    }\n  }\n\n}\n;\n\n/**\n * @brief Outputs a nicely formatted usage string with support for multi-column formatting\n * and line-wrapping.\n *\n * printUsage() takes the @c help texts of a Descriptor[] array and formats them into\n * a usage message, wrapping lines to achieve the desired output width.\n *\n * <b>Table formatting:</b>\n *\n * Aside from plain strings which are simply line-wrapped, the usage may contain tables. Tables\n * are used to align elements in the output.\n *\n * @code\n * // Without a table. The explanatory texts are not aligned.\n * -c, --create  |Creates something.\n * -k, --kill  |Destroys something.\n *\n * // With table formatting. The explanatory texts are aligned.\n * -c, --create  |Creates something.\n * -k, --kill    |Destroys something.\n * @endcode\n *\n * Table formatting removes the need to pad help texts manually with spaces to achieve\n * alignment. To create a table, simply insert \\\\t (tab) characters to separate the cells\n * within a row.\n *\n * @code\n * const option::Descriptor usage[] = {\n * {..., \"-c, --create  \\tCreates something.\" },\n * {..., \"-k, --kill  \\tDestroys something.\" }, ...\n * @endcode\n *\n * Note that you must include the minimum amount of space desired between cells yourself.\n * Table formatting will insert further spaces as needed to achieve alignment.\n *\n * You can insert line breaks within cells by using \\\\v (vertical tab).\n *\n * @code\n * const option::Descriptor usage[] = {\n * {..., \"-c,\\v--create  \\tCreates\\vsomething.\" },\n * {..., \"-k,\\v--kill  \\tDestroys\\vsomething.\" }, ...\n *\n * // results in\n *\n * -c,       Creates\n * --create  something.\n * -k,       Destroys\n * --kill    something.\n * @endcode\n *\n * You can mix lines that do not use \\\\t or \\\\v with those that do. The plain\n * lines will not mess up the table layout. Alignment of the table columns will\n * be maintained even across these interjections.\n *\n * @code\n * const option::Descriptor usage[] = {\n * {..., \"-c, --create  \\tCreates something.\" },\n * {..., \"----------------------------------\" },\n * {..., \"-k, --kill  \\tDestroys something.\" }, ...\n *\n * // results in\n *\n * -c, --create  Creates something.\n * ----------------------------------\n * -k, --kill    Destroys something.\n * @endcode\n *\n * You can have multiple tables within the same usage whose columns are\n * aligned independently. Simply insert a dummy Descriptor with @c help==0.\n *\n * @code\n * const option::Descriptor usage[] = {\n * {..., \"Long options:\" },\n * {..., \"--very-long-option  \\tDoes something long.\" },\n * {..., \"--ultra-super-mega-long-option  \\tTakes forever to complete.\" },\n * {..., 0 }, // ---------- table break -----------\n * {..., \"Short options:\" },\n * {..., \"-s  \\tShort.\" },\n * {..., \"-q  \\tQuick.\" }, ...\n *\n * // results in\n *\n * Long options:\n * --very-long-option              Does something long.\n * --ultra-super-mega-long-option  Takes forever to complete.\n * Short options:\n * -s  Short.\n * -q  Quick.\n *\n * // Without the table break it would be\n *\n * Long options:\n * --very-long-option              Does something long.\n * --ultra-super-mega-long-option  Takes forever to complete.\n * Short options:\n * -s                              Short.\n * -q                              Quick.\n * @endcode\n *\n * <b>Output methods:</b>\n *\n * Because TheLeanMeanC++Option parser is freestanding, you have to provide the means for\n * output in the first argument(s) to printUsage(). Because printUsage() is implemented as\n * a set of template functions, you have great flexibility in your choice of output\n * method. The following example demonstrates typical uses. Anything that's similar enough\n * will work.\n *\n * @code\n * #include <unistd.h>  // write()\n * #include <iostream>  // cout\n * #include <sstream>   // ostringstream\n * #include <cstdio>    // fwrite()\n * using namespace std;\n *\n * void my_write(const char* str, int size) {\n *   fwrite(str, size, 1, stdout);\n * }\n *\n * struct MyWriter {\n *   void write(const char* buf, size_t size) const {\n *      fwrite(str, size, 1, stdout);\n *   }\n * };\n *\n * struct MyWriteFunctor {\n *   void operator()(const char* buf, size_t size) {\n *      fwrite(str, size, 1, stdout);\n *   }\n * };\n * ...\n * printUsage(my_write, usage);    // custom write function\n * printUsage(MyWriter(), usage);  // temporary of a custom class\n * MyWriter writer;\n * printUsage(writer, usage);      // custom class object\n * MyWriteFunctor wfunctor;\n * printUsage(&wfunctor, usage);   // custom functor\n * printUsage(write, 1, usage);    // write() to file descriptor 1\n * printUsage(cout, usage);        // an ostream&\n * printUsage(fwrite, stdout, usage);  // fwrite() to stdout\n * ostringstream sstr;\n * printUsage(sstr, usage);        // an ostringstream&\n *\n * @endcode\n *\n * @par Notes:\n * @li the @c write() method of a class that is to be passed as a temporary\n *     as @c MyWriter() is in the example, must be a @c const method, because\n *     temporary objects are passed as const reference. This only applies to\n *     temporary objects that are created and destroyed in the same statement.\n *     If you create an object like @c writer in the example, this restriction\n *     does not apply.\n * @li a functor like @c MyWriteFunctor in the example must be passed as a pointer.\n *     This differs from the way functors are passed to e.g. the STL algorithms.\n * @li All printUsage() templates are tiny wrappers around a shared non-template implementation.\n *     So there's no penalty for using different versions in the same program.\n * @li printUsage() always interprets Descriptor::help as UTF-8 and always produces UTF-8-encoded\n *     output. If your system uses a different charset, you must do your own conversion. You\n *     may also need to change the font of the console to see non-ASCII characters properly.\n *     This is particularly true for Windows.\n * @li @b Security @b warning: Do not insert untrusted strings (such as user-supplied arguments)\n *     into the usage. printUsage() has no protection against malicious UTF-8 sequences.\n *\n * @param prn The output method to use. See the examples above.\n * @param usage the Descriptor[] array whose @c help texts will be formatted.\n * @param width the maximum number of characters per output line. Note that this number is\n *        in actual characters, not bytes. printUsage() supports UTF-8 in @c help and will\n *        count multi-byte UTF-8 sequences properly. Asian wide characters are counted\n *        as 2 characters.\n * @param last_column_min_percent (0-100) The minimum percentage of @c width that should be available\n *        for the last column (which typically contains the textual explanation of an option).\n *        If less space is available, the last column will be printed on its own line, indented\n *        according to @c last_column_own_line_max_percent.\n * @param last_column_own_line_max_percent (0-100) If the last column is printed on its own line due to\n *        less than @c last_column_min_percent of the width being available, then only\n *        @c last_column_own_line_max_percent of the extra line(s) will be used for the\n *        last column's text. This ensures an indentation. See example below.\n *\n * @code\n * // width=20, last_column_min_percent=50 (i.e. last col. min. width=10)\n * --3456789 1234567890\n *           1234567890\n *\n * // width=20, last_column_min_percent=75 (i.e. last col. min. width=15)\n * // last_column_own_line_max_percent=75\n * --3456789\n *      123456789012345\n *      67890\n *\n * // width=20, last_column_min_percent=75 (i.e. last col. min. width=15)\n * // last_column_own_line_max_percent=33 (i.e. max. 5)\n * --3456789\n *                12345\n *                67890\n *                12345\n *                67890\n * @endcode\n */\ntemplate<typename OStream>\nvoid printUsage(OStream& prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::OStreamWriter<OStream> write(prn);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\ntemplate<typename Function>\nvoid printUsage(Function* prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::FunctionWriter<Function> write(prn);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\ntemplate<typename Temporary>\nvoid printUsage(const Temporary& prn, const Descriptor usage[], int width = 80, int last_column_min_percent = 50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::TemporaryWriter<Temporary> write(prn);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\ntemplate<typename Syscall>\nvoid printUsage(Syscall* prn, int fd, const Descriptor usage[], int width = 80, int last_column_min_percent = 50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::SyscallWriter<Syscall> write(prn, fd);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\ntemplate<typename Function, typename Stream>\nvoid printUsage(Function* prn, Stream* stream, const Descriptor usage[], int width = 80, int last_column_min_percent =\n                    50,\n                int last_column_own_line_max_percent = 75)\n{\n  PrintUsageImplementation::StreamWriter<Function, Stream> write(prn, stream);\n  PrintUsageImplementation::printUsage(write, usage, width, last_column_min_percent, last_column_own_line_max_percent);\n}\n\n}\n// namespace option\n\n#endif /* OPTIONPARSER_H_ */\n"
  },
  {
    "path": "src/core/program.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"program.h\"\n\n#include <cassert>\n#include <iostream>\n#include <memory>\n#include <sstream>\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#include <GLES3/gl32.h>\n#else\n#include <GL/glew.h>\n#define GL_GLEXT_PROTOTYPES 1\n#include <SDL2/SDL_opengl.h>\n#include <SDL2/SDL_opengl_glext.h>\n#endif\n\n#include \"log.h\"\n#include \"util.h\"\n\n#ifndef NDEBUG\n#define WARNINGS_AS_ERRORS\n#endif\n\nstatic std::string ExpandMacros(std::vector<std::pair<std::string, std::string>> macros, const std::string& source)\n{\n    std::string result = source;\n\n    for (const auto& macro : macros)\n    {\n        std::string::size_type pos = 0;\n        while ((pos = result.find(macro.first, pos)) != std::string::npos)\n        {\n            result.replace(pos, macro.first.length(), macro.second);\n            // Move past the last replaced position\n            pos += macro.second.length();\n        }\n    }\n\n    return result;\n}\n\nstatic void DumpShaderSource(const std::string& source)\n{\n    std::stringstream ss(source);\n    std::string line;\n    int i = 1;\n    while (std::getline(ss, line))\n    {\n        Log::D(\"%04d: %s\\n\", i, line.c_str());\n        i++;\n    }\n    Log::D(\"\\n\");\n}\n\nstatic bool CompileShader(GLenum type, const std::string& source, GLint* shaderOut, const std::string& debugName)\n{\n    GLint shader = glCreateShader(type);\n    int size = static_cast<int>(source.size());\n    const GLchar* sourcePtr = source.c_str();\n    glShaderSource(shader, 1, (const GLchar**)&sourcePtr, &size);\n    glCompileShader(shader);\n\n    GLint compiled;\n    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);\n\n    if (!compiled)\n    {\n        Log::E(\"shader compilation error for \\\"%s\\\"!\\n\", debugName.c_str());\n    }\n\n    GLint bufferLen = 0;\n    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &bufferLen);\n    if (bufferLen > 1)\n    {\n        if (compiled)\n        {\n            Log::E(\"shader compilation warning for \\\"%s\\\"!\\n\", debugName.c_str());\n        }\n\n        GLsizei len = 0;\n        std::unique_ptr<char> buffer(new char[bufferLen]);\n        glGetShaderInfoLog(shader, bufferLen, &len, buffer.get());\n        Log::E(\"%s\\n\", buffer.get());\n        DumpShaderSource(source);\n    }\n\n#ifdef WARNINGS_AS_ERRORS\n    if (!compiled || bufferLen > 1)\n#else\n    if (!compiled)\n#endif\n    {\n        return false;\n    }\n\n    *shaderOut = shader;\n    return true;\n}\n\nProgram::Program() : program(0), vertShader(0), geomShader(0), fragShader(0), computeShader(0)\n{\n#ifdef __ANDROID__\n    AddMacro(\"HEADER\", \"#version 320 es\\nprecision highp float;\");\n#else\n    AddMacro(\"HEADER\", \"#version 460\");\n#endif\n}\n\nProgram::~Program()\n{\n    Delete();\n}\n\nvoid Program::AddMacro(const std::string& key, const std::string& value)\n{\n    // In order to keep the glsl code compiling if the macro is not applied.\n    // the key is enclosed in a c-style comment and double %.\n    std::string token = \"/*%%\" + key + \"%%*/\";\n    macros.push_back(std::pair(token, value));\n}\n\nbool Program::LoadVertFrag(const std::string& vertFilename, const std::string& fragFilename)\n{\n    return LoadVertGeomFrag(vertFilename, std::string(), fragFilename);\n}\n\nbool Program::LoadVertGeomFrag(const std::string& vertFilename, const std::string& geomFilename, const std::string& fragFilename)\n{\n    // Delete old shader/program\n    Delete();\n\n    const bool useGeomShader = !geomFilename.empty();\n\n    if (useGeomShader)\n    {\n        debugName = vertFilename + \" + \" + geomFilename + \" + \" + fragFilename;\n    }\n    else\n    {\n        debugName = vertFilename + \" + \" + fragFilename;\n    }\n\n    std::string vertSource, geomSource, fragSource;\n    if (!LoadFile(vertFilename, vertSource))\n    {\n        Log::E(\"Failed to load vertex shader %s\\n\", vertFilename.c_str());\n        return false;\n    }\n    vertSource = ExpandMacros(macros, vertSource);\n\n    if (useGeomShader)\n    {\n        if (!LoadFile(geomFilename, geomSource))\n        {\n            Log::E(\"Failed to load geometry shader %s\\n\", geomFilename.c_str());\n            return false;\n        }\n        geomSource = ExpandMacros(macros, geomSource);\n    }\n\n    if (!LoadFile(fragFilename, fragSource))\n    {\n        Log::E(\"Failed to load fragment shader \\\"%s\\\"\\n\", fragFilename.c_str());\n        return false;\n    }\n    fragSource = ExpandMacros(macros, fragSource);\n\n    if (!CompileShader(GL_VERTEX_SHADER, vertSource, &vertShader, vertFilename))\n    {\n        Log::E(\"Failed to compile vertex shader \\\"%s\\\"\\n\", vertFilename.c_str());\n        return false;\n    }\n\n    if (useGeomShader)\n    {\n        geomSource = ExpandMacros(macros, geomSource);\n        if (!CompileShader(GL_GEOMETRY_SHADER, geomSource, &geomShader, geomFilename))\n        {\n            Log::E(\"Failed to compile geometry shader \\\"%s\\\"\\n\", geomFilename.c_str());\n            return false;\n        }\n    }\n\n    if (!CompileShader(GL_FRAGMENT_SHADER, fragSource, &fragShader, fragFilename))\n    {\n        Log::E(\"Failed to compile fragment shader \\\"%s\\\"\\n\", fragFilename.c_str());\n        return false;\n    }\n\n    program = glCreateProgram();\n    glAttachShader(program, vertShader);\n    glAttachShader(program, fragShader);\n    if (useGeomShader)\n    {\n        glAttachShader(program, geomShader);\n    }\n    glLinkProgram(program);\n\n    if (!CheckLinkStatus())\n    {\n        Log::E(\"Failed to link program \\\"%s\\\"\\n\", debugName.c_str());\n\n        // dump shader source for reference\n        Log::D(\"\\n\");\n        Log::D(\"%s =\\n\", vertFilename.c_str());\n        DumpShaderSource(vertSource);\n        if (useGeomShader)\n        {\n            Log::D(\"%s =\\n\", geomFilename.c_str());\n            DumpShaderSource(geomSource);\n        }\n        Log::D(\"%s =\\n\", fragFilename.c_str());\n        DumpShaderSource(fragSource);\n\n        return false;\n    }\n\n    const int MAX_NAME_SIZE = 1028;\n    static char name[MAX_NAME_SIZE];\n\n    GLint numAttribs;\n    glGetProgramiv(program, GL_ACTIVE_ATTRIBUTES, &numAttribs);\n    for (int i = 0; i < numAttribs; ++i)\n    {\n        Variable v;\n        GLsizei strLen;\n        glGetActiveAttrib(program, i, MAX_NAME_SIZE, &strLen, &v.size, &v.type, name);\n        v.loc = glGetAttribLocation(program, name);\n        attribs[name] = v;\n    }\n\n    GLint numUniforms;\n    glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &numUniforms);\n    for (int i = 0; i < numUniforms; ++i)\n    {\n        Variable v;\n        GLsizei strLen;\n        glGetActiveUniform(program, i, MAX_NAME_SIZE, &strLen, &v.size, &v.type, name);\n        int loc = glGetUniformLocation(program, name);\n        v.loc = loc;\n        uniforms[name] = v;\n    }\n\n    return true;\n}\n\nbool Program::LoadCompute(const std::string& computeFilename)\n{\n    // Delete old shader/program\n    Delete();\n\n    debugName = computeFilename;\n\n    GL_ERROR_CHECK(\"Program::LoadCompute begin\");\n\n    std::string computeSource;\n    if (!LoadFile(computeFilename, computeSource))\n    {\n        Log::E(\"Failed to load compute shader \\\"%s\\\"\\n\", computeFilename.c_str());\n        return false;\n    }\n\n    GL_ERROR_CHECK(\"Program::LoadCompute LoadFile\");\n\n    computeSource = ExpandMacros(macros, computeSource);\n    if (!CompileShader(GL_COMPUTE_SHADER, computeSource, &computeShader, computeFilename))\n    {\n        Log::E(\"Failed to compile compute shader \\\"%s\\\"\\n\", computeFilename.c_str());\n        return false;\n    }\n\n    GL_ERROR_CHECK(\"Program::LoadCompute CompileShader\");\n\n    program = glCreateProgram();\n    glAttachShader(program, computeShader);\n    glLinkProgram(program);\n\n    GL_ERROR_CHECK(\"Program::LoadCompute Attach and Link\");\n\n    if (!CheckLinkStatus())\n    {\n        Log::E(\"Failed to link program \\\"%s\\\"\\n\", debugName.c_str());\n\n        // dump shader source for reference\n        Log::D(\"\\n\");\n        Log::D(\"%s =\\n\", computeFilename.c_str());\n        DumpShaderSource(computeSource);\n\n        return false;\n    }\n\n    const int MAX_NAME_SIZE = 1028;\n    static char name[MAX_NAME_SIZE];\n\n    GLint numUniforms;\n    glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &numUniforms);\n    for (int i = 0; i < numUniforms; ++i)\n    {\n        Variable v;\n        GLsizei strLen;\n        glGetActiveUniform(program, i, MAX_NAME_SIZE, &strLen, &v.size, &v.type, name);\n        int loc = glGetUniformLocation(program, name);\n        v.loc = loc;\n        uniforms[name] = v;\n    }\n\n    GL_ERROR_CHECK(\"Program::LoadCompute get uniforms\");\n\n    // TODO: build reflection info on shader storage blocks\n\n    return true;\n}\n\nvoid Program::Bind() const\n{\n    glUseProgram(program);\n}\n\nint Program::GetUniformLoc(const std::string& name) const\n{\n    auto iter = uniforms.find(name);\n    if (iter != uniforms.end())\n    {\n        return iter->second.loc;\n    }\n    else\n    {\n        assert(false);\n        Log::W(\"Could not find uniform \\\"%s\\\" for program \\\"%s\\\"\\n\", name.c_str(), debugName.c_str());\n        return 0;\n    }\n}\n\nint Program::GetAttribLoc(const std::string& name) const\n{\n    auto iter = attribs.find(name);\n    if (iter != attribs.end())\n    {\n        return iter->second.loc;\n    }\n    else\n    {\n        Log::W(\"Could not find attrib \\\"%s\\\" for program \\\"%s\\\"\\n\", name.c_str(), debugName.c_str());\n        assert(false);\n        return 0;\n    }\n}\n\nvoid Program::SetUniformRaw(int loc, uint32_t value) const\n{\n    glUniform1ui(loc, value);\n}\n\nvoid Program::SetUniformRaw(int loc, int32_t value) const\n{\n    glUniform1i(loc, value);\n}\n\nvoid Program::SetUniformRaw(int loc, float value) const\n{\n    glUniform1f(loc, value);\n}\n\nvoid Program::SetUniformRaw(int loc, const glm::vec2& value) const\n{\n    glUniform2fv(loc, 1, (float*)&value);\n}\n\nvoid Program::SetUniformRaw(int loc, const glm::vec3& value) const\n{\n    glUniform3fv(loc, 1, (float*)&value);\n}\n\nvoid Program::SetUniformRaw(int loc, const glm::vec4& value) const\n{\n    glUniform4fv(loc, 1, (float*)&value);\n}\n\nvoid Program::SetUniformRaw(int loc, const glm::mat2& value) const\n{\n    glUniformMatrix2fv(loc, 1, GL_FALSE, (float*)&value);\n}\n\nvoid Program::SetUniformRaw(int loc, const glm::mat3& value) const\n{\n    glUniformMatrix3fv(loc, 1, GL_FALSE, (float*)&value);\n}\n\nvoid Program::SetUniformRaw(int loc, const glm::mat4& value) const\n{\n    glUniformMatrix4fv(loc, 1, GL_FALSE, (float*)&value);\n}\n\nvoid Program::SetAttribRaw(int loc, float* values, size_t stride) const\n{\n    glVertexAttribPointer(loc, 1, GL_FLOAT, GL_FALSE, (GLsizei)stride, values);\n    glEnableVertexAttribArray(loc);\n}\n\nvoid Program::SetAttribRaw(int loc, glm::vec2* values, size_t stride) const\n{\n    glVertexAttribPointer(loc, 2, GL_FLOAT, GL_FALSE, (GLsizei)stride, values);\n    glEnableVertexAttribArray(loc);\n}\n\nvoid Program::SetAttribRaw(int loc, glm::vec3* values, size_t stride) const\n{\n    glVertexAttribPointer(loc, 3, GL_FLOAT, GL_FALSE, (GLsizei)stride, values);\n    glEnableVertexAttribArray(loc);\n}\n\nvoid Program::SetAttribRaw(int loc, glm::vec4* values, size_t stride) const\n{\n    glVertexAttribPointer(loc, 4, GL_FLOAT, GL_FALSE, (GLsizei)stride, values);\n    glEnableVertexAttribArray(loc);\n}\n\nvoid Program::Delete()\n{\n    debugName = \"\";\n\n    if (vertShader > 0)\n    {\n        glDeleteShader(vertShader);\n        vertShader = 0;\n    }\n\n    if (geomShader > 0)\n    {\n        glDeleteShader(geomShader);\n        geomShader = 0;\n    }\n\n    if (fragShader > 0)\n    {\n        glDeleteShader(fragShader);\n        fragShader = 0;\n    }\n\n    if (computeShader > 0)\n    {\n        glDeleteShader(computeShader);\n        computeShader = 0;\n    }\n\n    if (program > 0)\n    {\n        glDeleteProgram(program);\n        program = 0;\n    }\n\n    uniforms.clear();\n    attribs.clear();\n}\n\nbool Program::CheckLinkStatus()\n{\n    GLint linked;\n    glGetProgramiv(program, GL_LINK_STATUS, &linked);\n\n    if (!linked)\n    {\n        Log::E(\"Failed to link shaders \\\"%s\\\"\\n\", debugName.c_str());\n    }\n\n    const GLint MAX_BUFFER_LEN = 4096;\n    GLsizei bufferLen = 0;\n    std::unique_ptr<char> buffer(new char[MAX_BUFFER_LEN]);\n    glGetProgramInfoLog(program, MAX_BUFFER_LEN, &bufferLen, buffer.get());\n    if (bufferLen > 0)\n    {\n        if (linked)\n        {\n            Log::W(\"Warning during linking shaders \\\"%s\\\"\\n\", debugName.c_str());\n        }\n        Log::W(\"%s\\n\", buffer.get());\n    }\n\n#ifdef WARNINGS_AS_ERRORS\n    if (!linked || bufferLen > 1)\n#else\n    if (!linked)\n#endif\n    {\n        return false;\n    }\n\n    return true;\n}\n"
  },
  {
    "path": "src/core/program.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <iostream>\n#include <unordered_map>\n#include <vector>\n\n#include \"core/log.h\"\n\nclass Program\n{\npublic:\n    Program();\n    ~Program();\n\n    // used to inject #defines or other code into shaders\n    // AddMacro(\"FOO\", \"BAR\");\n    // will replace the string /*%%FOO%%*/ in the source shader with BAR\n    void AddMacro(const std::string& key, const std::string& value);\n\n    bool LoadVertFrag(const std::string& vertFilename, const std::string& fragFilename);\n    bool LoadVertGeomFrag(const std::string& vertFilename, const std::string& geomFilename, const std::string& fragFilename);\n    bool LoadCompute(const std::string& computeFilename);\n    void Bind() const;\n\n    int GetUniformLoc(const std::string& name) const;\n    int GetAttribLoc(const std::string& name) const;\n\n    template <typename T>\n    void SetUniform(const std::string& name, T value) const\n    {\n        auto iter = uniforms.find(name);\n        if (iter != uniforms.end())\n        {\n            SetUniformRaw(iter->second.loc, value);\n        }\n        else\n        {\n            Log::W(\"Could not find uniform \\\"%s\\\" for program \\\"%s\\\"\\n\", name.c_str(), debugName.c_str());\n        }\n    }\n\n    void SetUniformRaw(int loc, int32_t value) const;\n    void SetUniformRaw(int loc, uint32_t value) const;\n    void SetUniformRaw(int loc, float value) const;\n    void SetUniformRaw(int loc, const glm::vec2& value) const;\n    void SetUniformRaw(int loc, const glm::vec3& value) const;\n    void SetUniformRaw(int loc, const glm::vec4& value) const;\n    void SetUniformRaw(int loc, const glm::mat2& value) const;\n    void SetUniformRaw(int loc, const glm::mat3& value) const;\n    void SetUniformRaw(int loc, const glm::mat4& value) const;\n\n    template <typename T>\n    void SetAttrib(const std::string& name, T* values, size_t stride = 0) const\n    {\n        auto iter = attribs.find(name);\n        if (iter != attribs.end())\n        {\n            SetAttribRaw(iter->second.loc, values, stride);\n        }\n        else\n        {\n            Log::W(\"Could not find attrib \\\"%s\\\" for program \\\"%s\\\"\\n\", name.c_str(), debugName.c_str());\n        }\n    }\n\n    void SetAttribRaw(int loc, float* values, size_t stride = 0) const;\n    void SetAttribRaw(int loc, glm::vec2* values, size_t stride = 0) const;\n    void SetAttribRaw(int loc, glm::vec3* values, size_t stride = 0) const;\n    void SetAttribRaw(int loc, glm::vec4* values, size_t stride = 0) const;\n\nprotected:\n\n    void Delete();\n    bool CheckLinkStatus();\n\n    int program;\n    int vertShader;\n    int geomShader;\n    int fragShader;\n    int computeShader;\n\n    struct Variable\n    {\n        int size;\n        uint32_t type;\n        int loc;\n    };\n\n    std::unordered_map<std::string, Variable> uniforms;\n    std::unordered_map<std::string, Variable> attribs;\n    std::vector<std::pair<std::string, std::string>> macros;\n    std::string debugName;\n};\n"
  },
  {
    "path": "src/core/statemachine.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <functional>\n#include <map>\n#include <vector>\n\n#include \"log.h\"\n\ntemplate <typename State>\nclass StateMachine\n{\npublic:\n    using ProcessCallback = std::function<void(float)>;\n    using VoidCallback = std::function<void(void)>;\n    using BoolCallback = std::function<bool(void)>;\n\nprotected:\n    struct TransitionStruct\n    {\n        TransitionStruct(const BoolCallback& cbIn, State stateIn, std::string nameIn) : cb(cbIn), state(stateIn), name(nameIn) {}\n        BoolCallback cb;\n        State state;\n        std::string name;\n    };\n    struct StateStruct\n    {\n        StateStruct(const VoidCallback& enterIn, const VoidCallback& exitIn, const ProcessCallback& processIn) : enter(enterIn), exit(exitIn), process(processIn) {}\n        VoidCallback enter;\n        VoidCallback exit;\n        ProcessCallback process;\n        std::vector<TransitionStruct> transitionVec;\n    };\n    using StatePair = std::pair<State, StateStruct>;\n\npublic:\n    StateMachine(State defaultState) : state(defaultState), debug(false)\n    {\n        ;\n    }\n\n    void AddState(State state, const std::string& name, const VoidCallback& enter, const VoidCallback& exit, const ProcessCallback& process)\n    {\n        StatePair sp(state, StateStruct(enter, exit, process));\n        stateStructMap.insert(sp);\n        stateNameMap.insert(std::pair<State, std::string>(state, name));\n    }\n\n    void AddTransition(State state, State newState, const std::string& name, const BoolCallback& transitionCb)\n    {\n        stateStructMap.at(state).transitionVec.push_back(TransitionStruct(transitionCb, newState, name));\n    }\n\n    void Process(float dt)\n    {\n        for (auto&& trans : stateStructMap.at(state).transitionVec)\n        {\n            if (trans.cb())\n            {\n                ChangeState(trans.state, trans.name);\n            }\n        }\n        stateStructMap.at(state).process(dt);\n    }\n\n    void ChangeState(State newState, const std::string& reason)\n    {\n        if (debug)\n        {\n            Log::D(\"StateChange from %s -> %s, (%s)\\n\", stateNameMap.at(state).c_str(), stateNameMap.at(newState).c_str(), reason.c_str());\n        }\n        stateStructMap.at(state).exit();\n        stateStructMap.at(newState).enter();\n        state = newState;\n    }\n\n    void SetDebug(bool debugIn) { debug = debugIn; }\n\nprotected:\n\n    State state;\n    std::map<State, StateStruct> stateStructMap;\n    std::map<State, std::string> stateNameMap;\n    bool debug;\n};\n"
  },
  {
    "path": "src/core/textrenderer.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"textrenderer.h\"\n\n#include <fstream>\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#include <GLES3/gl32.h>\n#else\n#include <GL/glew.h>\n#endif\n\n#include <nlohmann/json.hpp>\n\n#include \"core/image.h\"\n#include \"core/log.h\"\n#include \"core/util.h\"\n#include \"core/program.h\"\n#include \"core/texture.h\"\n\nconst int TAB_SIZE = 4;\nstatic TextRenderer::TextKey nextKey = 1;\n\nTextRenderer::TextRenderer()\n{\n\n}\n\nbool TextRenderer::Init(const std::string& fontJsonFilename, const std::string& fontPngFilename)\n{\n    std::ifstream f(GetRootPath() + fontJsonFilename);\n    if (f.fail())\n    {\n        return false;\n    }\n\n    try\n    {\n        nlohmann::json j = nlohmann::json::parse(f);\n        textureWidth = j[\"texture_width\"].template get<float>();\n        nlohmann::json metrics = j[\"glyph_metrics\"];\n        for (auto& iter : metrics.items())\n        {\n            int key = iter.value()[\"ascii_index\"].template get<int>();\n            if (key < 0 && key > std::numeric_limits<uint8_t>::max())\n            {\n                Log::W(\"TextRenderer(%s) glyph %d is out of range\\n\", fontJsonFilename.c_str(), key);\n                continue;\n            }\n            Glyph g;\n            nlohmann::json v2 = iter.value()[\"xy_lower_left\"];\n            g.xyMin = glm::vec2(v2[0].template get<float>(), v2[1].template get<float>());\n            v2 = iter.value()[\"xy_upper_right\"];\n            g.xyMax = glm::vec2(v2[0].template get<float>(), v2[1].template get<float>());\n            v2 = iter.value()[\"uv_lower_left\"];\n            g.uvMin = glm::vec2(v2[0].template get<float>(), v2[1].template get<float>());\n            v2 = iter.value()[\"uv_upper_right\"];\n            g.uvMax = glm::vec2(v2[0].template get<float>(), v2[1].template get<float>());\n            v2 = iter.value()[\"advance\"];\n            g.advance = glm::vec2(v2[0].template get<float>(), v2[1].template get<float>());\n\n            glyphMap.insert(std::pair<uint8_t, Glyph>((uint8_t)key, g));\n        }\n        // TODO: support kerning table, for variable width fonts\n    }\n    catch (const nlohmann::json::exception& e)\n    {\n        std::string s = e.what();\n        Log::E(\"TextRenderer::Init(%s) exception: %s\\n\", fontJsonFilename.c_str(), s.c_str());\n        return false;\n    }\n\n    // find the spaceGlyph\n    auto gIter = glyphMap.find((uint8_t)' ');\n    assert(gIter != glyphMap.end());\n    if (gIter != glyphMap.end())\n    {\n        spaceGlyph = gIter->second;\n    }\n\n    Image fontImg;\n    if (!fontImg.Load(fontPngFilename))\n    {\n        Log::E(\"Error loading fontPng\\n\");\n        return false;\n    }\n\n    // TODO: get gamma correct.\n    //fontImg.isSRGB = isFramebufferSRGBEnabled;\n\n    Texture::Params texParams = {FilterType::LinearMipmapLinear, FilterType::Linear, WrapType::ClampToEdge, WrapType::ClampToEdge};\n    fontTex = std::make_shared<Texture>(fontImg, texParams);\n\n    textProg = std::make_shared<Program>();\n    if (!textProg->LoadVertFrag(\"shader/text_vert.glsl\", \"shader/text_frag.glsl\"))\n    {\n        Log::E(\"Error loading TextRenderer shader!\\n\");\n        return false;\n    }\n\n    return true;\n}\n\nvoid TextRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                          const glm::vec4& viewport, const glm::vec2& nearFar)\n{\n    textProg->Bind();\n\n    // use texture unit 0 for fontTexture\n    glActiveTexture(GL_TEXTURE0);\n    glBindTexture(GL_TEXTURE_2D, fontTex->texture);\n    textProg->SetUniform(\"fontTex\", 0);\n\n    glm::mat4 viewProjMat = projMat * glm::inverse(cameraMat);\n    float aspect = viewport.w / viewport.z;\n    glm::mat4 aspectMat = MakeMat4(glm::vec3(aspect, 1.0f, 1.0f), glm::quat(), glm::vec3(-aspect / aspect, 0.0f, 0.0f));\n    for (auto&& tIter : textMap)\n    {\n        if (tIter.second.isScreenAligned)\n        {\n            textProg->SetUniform(\"modelViewProjMat\", aspectMat * tIter.second.xform);\n        }\n        else\n        {\n            textProg->SetUniform(\"modelViewProjMat\", viewProjMat * tIter.second.xform);\n        }\n        textProg->SetAttrib(\"position\", tIter.second.posVec.data());\n        textProg->SetAttrib(\"uv\", tIter.second.uvVec.data());\n        textProg->SetAttrib(\"color\", tIter.second.colorVec.data());\n\n        glDrawArrays(GL_TRIANGLES, 0, (GLsizei)tIter.second.posVec.size());\n    }\n}\n\n// creates a new text and adds it to the scene\nTextRenderer::TextKey TextRenderer::AddWorldText(const glm::mat4 xform, const glm::vec4& color,\n                                                 float lineHeight, const std::string& asciiString)\n{\n    Text text;\n    text.xform = xform;\n    text.posVec.reserve(asciiString.size() * 6);\n    text.uvVec.reserve(asciiString.size() * 6);\n    text.colorVec.reserve(asciiString.size() * 6);\n    text.isScreenAligned = false;\n\n    glm::vec3 pen(0.0f, 0.0f, 0.0f);\n    BuildText(text, pen, lineHeight, color, asciiString);\n\n    uint32_t textKey = nextKey++;\n    textMap.insert(std::pair<uint32_t, Text>(textKey, text));\n\n    return textKey;\n}\n\nTextRenderer::TextKey TextRenderer::AddScreenText(const glm::ivec2& pos, int numRows, const glm::vec4& color,\n                                                  const std::string& asciiString)\n{\n    const bool addDropShadow = false;\n    return AddScreenTextImpl(pos, numRows, color, asciiString, addDropShadow, glm::vec4());\n}\n\nTextRenderer::TextKey TextRenderer::AddScreenTextWithDropShadow(const glm::ivec2& pos, int numRows, const glm::vec4& color,\n                                                                const glm::vec4& shadowColor, const std::string& asciiString)\n{\n    const bool addDropShadow = true;\n    return AddScreenTextImpl(pos, numRows, color, asciiString, addDropShadow, shadowColor);\n}\n\nvoid TextRenderer::SetTextXform(TextKey key, const glm::mat4 xform)\n{\n    auto tIter = textMap.find(key);\n    if (tIter != textMap.end())\n    {\n        tIter->second.xform = xform;\n    }\n}\n\n// removes text object form the scene\nvoid TextRenderer::RemoveText(TextKey key)\n{\n    textMap.erase(key);\n}\n\nvoid TextRenderer::BuildText(Text& text, const glm::vec3& pen, float lineHeight, const glm::vec4& color,\n                             const std::string& asciiString) const\n{\n    int r = 0;\n    int c = 0;\n    glm::vec2 penxy = pen;\n    float depth = pen.z;\n    for (auto&& ch : asciiString)\n    {\n        if (ch == ' ')\n        {\n            penxy += lineHeight * spaceGlyph.advance;\n            c++;\n        }\n        else if (ch == '\\n')\n        {\n            penxy = lineHeight * glm::vec2(0.0f, (float)-(r + 1));\n            r++;\n        }\n        else if (ch == '\\t')\n        {\n            int numSpaces = TAB_SIZE - (c % TAB_SIZE);\n            penxy += lineHeight * (float)numSpaces * spaceGlyph.advance;\n            c += numSpaces;\n        }\n        else\n        {\n            auto gIter = glyphMap.find((uint8_t)ch);\n            if (gIter == glyphMap.end())\n            {\n                continue;\n            }\n            const Glyph& g = gIter->second;\n\n            text.posVec.push_back(glm::vec3(penxy + lineHeight * g.xyMin, depth));\n            text.posVec.push_back(glm::vec3(penxy + lineHeight * g.xyMax, depth));\n            text.posVec.push_back(glm::vec3(penxy + lineHeight * glm::vec2(g.xyMin.x, g.xyMax.y), depth));\n            text.posVec.push_back(glm::vec3(penxy + lineHeight * g.xyMin, depth));\n            text.posVec.push_back(glm::vec3(penxy + lineHeight * glm::vec2(g.xyMax.x, g.xyMin.y), depth));\n            text.posVec.push_back(glm::vec3(penxy + lineHeight * g.xyMax, depth));\n            text.uvVec.push_back(g.uvMin);\n            text.uvVec.push_back(g.uvMax);\n            text.uvVec.push_back(glm::vec2(g.uvMin.x, g.uvMax.y));\n            text.uvVec.push_back(g.uvMin);\n            text.uvVec.push_back(glm::vec2(g.uvMax.x, g.uvMin.y));\n            text.uvVec.push_back(g.uvMax);\n            for (int i = 0; i < 6; i++)\n            {\n                text.colorVec.push_back(color);\n            }\n\n            penxy += lineHeight * g.advance;\n            c++;\n        }\n    }\n}\n\nTextRenderer::TextKey TextRenderer::AddScreenTextImpl(const glm::ivec2& pos, int numRows, const glm::vec4& color,\n                                                      const std::string& asciiString, bool addDropShadow,\n                                                      const glm::vec4& shadowColor)\n{\n    const float TEXT_LINE_HEIGHT = 2.0f / numRows;\n    glm::vec3 origin(0.1f * TEXT_LINE_HEIGHT, 1.0f - 0.75f * TEXT_LINE_HEIGHT, 0.0f);\n    glm::vec3 offset((float)pos.x * spaceGlyph.advance.x * TEXT_LINE_HEIGHT, (float)pos.y * -TEXT_LINE_HEIGHT, 0.0f);\n    Text text;\n    text.xform = MakeMat4(glm::quat(), origin + offset);\n    size_t vecSize = addDropShadow ? (asciiString.size() * 6 * 2) : (asciiString.size() * 6);\n    text.posVec.reserve(vecSize);\n    text.uvVec.reserve(vecSize);\n    text.colorVec.reserve(vecSize);\n    text.isScreenAligned = true;\n\n    if (addDropShadow)\n    {\n        glm::vec3 shadowPen = glm::vec3(0.05f * TEXT_LINE_HEIGHT, -0.05f * TEXT_LINE_HEIGHT, 0.1f);\n        BuildText(text, shadowPen, TEXT_LINE_HEIGHT, shadowColor, asciiString);\n    }\n\n    glm::vec3 pen(0.0f, 0.0f, 0.0f);\n    BuildText(text, pen, TEXT_LINE_HEIGHT, color, asciiString);\n\n    uint32_t textKey = nextKey++;\n    textMap.insert(std::pair<uint32_t, Text>(textKey, text));\n\n    return textKey;\n}\n\n\n"
  },
  {
    "path": "src/core/textrenderer.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <array>\n#include <glm/glm.hpp>\n#include <memory>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\nclass Program;\nstruct Texture;\n\nclass TextRenderer\n{\npublic:\n\n\tTextRenderer();\n\n\tbool Init(const std::string& fontJsonFilename, const std::string& fontPngFilename);\n\n\t// viewport = (x, y, width, height)\n\tvoid Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                const glm::vec4& viewport, const glm::vec2& nearFar);\n\n    using TextKey = uint32_t;\n\n    // creates a new text and adds it to the scene\n    TextKey AddWorldText(const glm::mat4 xform, const glm::vec4& color,\n                    float lineHeight, const std::string& asciiString);\n    TextKey AddScreenText(const glm::ivec2& pos, int numRows, const glm::vec4& color,\n                          const std::string& asciiString);\n    TextKey AddScreenTextWithDropShadow(const glm::ivec2& pos, int numRows, const glm::vec4& color,\n                                        const glm::vec4& shadowColor, const std::string& asciiString);\n    void SetTextXform(TextKey key, const glm::mat4 xform);\n\n    // removes text object form the scene\n    void RemoveText(TextKey key);\n\nprotected:\n\n    struct Glyph\n    {\n        glm::vec2 xyMin;\n        glm::vec2 xyMax;\n        glm::vec2 uvMin;\n        glm::vec2 uvMax;\n        glm::vec2 advance;\n    };\n\n    struct Text\n    {\n        glm::mat4 xform;\n        std::vector<glm::vec3> posVec;\n        std::vector<glm::vec2> uvVec;\n        std::vector<glm::vec4> colorVec;\n        bool isScreenAligned;\n    };\n\n    void BuildText(Text& text, const glm::vec3& pen, float lineHeight, const glm::vec4& color,\n                   const std::string& asciiString) const;\n    TextKey AddScreenTextImpl(const glm::ivec2& pos, int numRows, const glm::vec4& color,\n                              const std::string& asciiString, bool addDropShadow, const glm::vec4& shadowColor);\n\n    std::unordered_map<uint8_t, Glyph> glyphMap;\n    float textureWidth;\n    std::shared_ptr<Program> textProg;\n    std::shared_ptr<Texture> fontTex;\n    std::unordered_map<uint32_t, Text> textMap;\n    Glyph spaceGlyph;\n};\n\n\n"
  },
  {
    "path": "src/core/texture.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"texture.h\"\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#else\n#include <GL/glew.h>\n#define GL_GLEXT_PROTOTYPES 1\n#include <SDL2/SDL_opengl.h>\n#include <SDL2/SDL_opengl_glext.h>\n#endif\n\n#include \"core/image.h\"\n\nstatic GLenum filterTypeToGL[] = {\n    GL_NEAREST,\n    GL_LINEAR,\n    GL_NEAREST_MIPMAP_NEAREST,\n    GL_LINEAR_MIPMAP_NEAREST,\n    GL_NEAREST_MIPMAP_LINEAR,\n    GL_LINEAR_MIPMAP_LINEAR\n};\n\nstatic GLenum wrapTypeToGL[] = {\n    GL_REPEAT,\n    GL_MIRRORED_REPEAT,\n    GL_CLAMP_TO_EDGE,\n#ifndef __ANDROID__\n    GL_MIRROR_CLAMP_TO_EDGE,\n#endif\n};\n\nstatic GLenum pixelFormatToGL[] = {\n    GL_LUMINANCE,\n    GL_LUMINANCE_ALPHA,\n    GL_RGB,\n    GL_RGBA\n};\n\nTexture::Texture(const Image& image, const Params& params)\n{\n    glGenTextures(1, &texture);\n    glBindTexture(GL_TEXTURE_2D, texture);\n\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterTypeToGL[(int)params.minFilter]);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterTypeToGL[(int)params.magFilter]);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapTypeToGL[(int)params.sWrap]);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapTypeToGL[(int)params.tWrap]);\n\n    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);\n\n    GLenum pf = pixelFormatToGL[(int)image.pixelFormat];\n\n    int internalFormat = pf;\n\n    if (image.isSRGB && pf == GL_RGB)\n    {\n        internalFormat = GL_SRGB8;\n    }\n    else if (image.isSRGB && pf == GL_RGBA)\n    {\n        internalFormat = GL_SRGB8_ALPHA8;\n    }\n\n    glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, image.width, image.height, 0, pf, GL_UNSIGNED_BYTE, &image.data[0]);\n\n    if ((int)params.minFilter >= (int)FilterType::NearestMipmapNearest)\n    {\n        glGenerateMipmap(GL_TEXTURE_2D);\n    }\n\n    if (image.pixelFormat == PixelFormat::RA || image.pixelFormat == PixelFormat::RGBA)\n    {\n        hasAlphaChannel = true;\n    }\n}\n\nTexture::Texture(uint32_t width, uint32_t height, uint32_t internalFormat,\n                 uint32_t format, uint32_t type, const Params& params)\n{\n    glGenTextures(1, &texture);\n    glBindTexture(GL_TEXTURE_2D, texture);\n\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filterTypeToGL[(int)params.minFilter]);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filterTypeToGL[(int)params.magFilter]);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapTypeToGL[(int)params.sWrap]);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapTypeToGL[(int)params.tWrap]);\n\n    glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, type, nullptr);\n}\n\nTexture::~Texture()\n{\n    glDeleteTextures(1, &texture);\n}\n\nvoid Texture::Bind(int unit) const\n{\n    glActiveTexture(GL_TEXTURE0 + unit);\n    glBindTexture(GL_TEXTURE_2D, texture);\n}\n"
  },
  {
    "path": "src/core/texture.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <stdint.h>\n\nstruct Image;\n\nenum class FilterType\n{\n    Nearest = 0,\n    Linear,\n    NearestMipmapNearest,\n    LinearMipmapNearest,\n    NearestMipmapLinear,\n    LinearMipmapLinear\n};\n\nenum class WrapType\n{\n    Repeat = 0,\n    MirroredRepeat,\n    ClampToEdge,\n    MirrorClampToEdge\n};\n\nstruct Texture\n{\n    struct Params\n    {\n        FilterType minFilter;\n        FilterType magFilter;\n        WrapType sWrap;\n        WrapType tWrap;\n    };\n\n    Texture(const Image& image, const Params& params);\n    Texture(uint32_t width, uint32_t height, uint32_t internalFormat,\n            uint32_t format, uint32_t type, const Params& params);\n    ~Texture();\n\n    void Bind(int unit) const;\n\n    uint32_t texture;\n    bool hasAlphaChannel;\n};\n"
  },
  {
    "path": "src/core/util.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"util.h\"\n\n#include <cassert>\n#include <fstream>\n#include <string.h>\n\n#include <glm/gtc/constants.hpp>\n#include <glm/gtc/matrix_transform.hpp>\n#include <glm/gtc/random.hpp>\n\n#ifndef __ANDROID__\n#include <GL/glew.h>\n#define GL_GLEXT_PROTOTYPES 1\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_opengl.h>\n#include <SDL2/SDL_opengl_glext.h>\n#else\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#endif\n\n#include \"log.h\"\n\nbool LoadFile(const std::string& filename, std::string& data)\n{\n    std::ifstream ifs(GetRootPath() + filename, std::ifstream::in);\n    if (ifs.good())\n    {\n        std::string content((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());\n        data = std::move(content);\n        return true;\n    }\n    else\n    {\n        return false;\n    }\n}\n\nbool SaveFile(const std::string& filename, const std::string& data)\n{\n    std::ofstream ofs(GetRootPath() + filename, std::ofstream::out);\n    if (ofs.good())\n    {\n        ofs << data;\n        return true;\n    }\n    else\n    {\n        return false;\n    }\n}\n\n// returns the number of bytes to advance\n// fills cp_out with the code point at p.\nint NextCodePointUTF8(const char *str, uint32_t *codePointOut)\n{\n    const uint8_t* p = (const uint8_t*)str;\n    if ((*p & 0x80) == 0)\n    {\n        *codePointOut = *p;\n        return 1;\n    }\n    else if ((*p & 0xe0) == 0xc0) // 110xxxxx 10xxxxxx\n    {\n        *codePointOut = ((*p & ~0xe0) << 6) | (*(p+1) & ~0xc0);\n        return 2;\n    }\n    else if ((*p & 0xf0) == 0xe0) // 1110xxxx 10xxxxxx 10xxxxxx\n    {\n        *codePointOut = ((*p & ~0xf0) << 12) | ((*(p+1) & ~0xc0) << 6) | (*(p+2) & ~0xc0);\n        return 3;\n    }\n    else if ((*p & 0xf8) == 0xf0) // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx\n    {\n        *codePointOut = ((*p & ~0xf8) << 18) | ((*(p+1) & ~0xc0) << 12) | ((*(p+1) & ~0xc0) << 6) | (*(p+2) & ~0xc0);\n        return 4;\n    }\n    else\n    {\n        // p is not at a valid starting point. p is not utf8 encoded or is at a bad offset.\n        assert(0);\n        *codePointOut = 0;\n        return 1;\n    }\n}\n\n#ifndef NDEBUG\n// If there is a glError this outputs it along with a message to stderr.\n// otherwise there is no output.\nvoid GLErrorCheck(const char* message)\n{\n    GLenum val = glGetError();\n    switch (val)\n    {\n    case GL_INVALID_ENUM:\n        Log::D(\"GL_INVALID_ENUM : %s\\n\", message);\n        break;\n    case GL_INVALID_VALUE:\n        Log::D(\"GL_INVALID_VALUE : %s\\n\", message);\n        break;\n    case GL_INVALID_OPERATION:\n        Log::D(\"GL_INVALID_OPERATION : %s\\n\", message);\n        break;\n#ifndef GL_ES_VERSION_2_0\n    case GL_STACK_OVERFLOW:\n        Log::D(\"GL_STACK_OVERFLOW : %s\\n\", message);\n        break;\n    case GL_STACK_UNDERFLOW:\n        Log::D(\"GL_STACK_UNDERFLOW : %s\\n\", message);\n        break;\n#endif\n    case GL_OUT_OF_MEMORY:\n        Log::D(\"GL_OUT_OF_MEMORY : %s\\n\", message);\n        break;\n    case GL_NO_ERROR:\n        break;\n    }\n}\n#endif\n\nglm::vec3 SafeNormalize(const glm::vec3& v, const glm::vec3& ifZero)\n{\n    float len = glm::length(v);\n    if (len > 0.0f)\n    {\n        return glm::normalize(v);\n    }\n    else\n    {\n        return ifZero;\n    }\n}\n\nglm::quat SafeMix(const glm::quat& a, const glm::quat& b, float alpha)\n{\n    // adjust signs if necessary\n    glm::quat bTemp = b;\n    float dot = glm::dot(a, bTemp);\n    if (dot < 0.0f)\n    {\n        bTemp = -bTemp;\n    }\n    return glm::normalize(glm::lerp(a, bTemp, alpha));\n}\n\nglm::mat3 MakeMat3(const glm::quat& rotation)\n{\n    glm::vec3 xAxis = rotation * glm::vec3(1.0f, 0.0f, 0.0f);\n    glm::vec3 yAxis = rotation * glm::vec3(0.0f, 1.0f, 0.0f);\n    glm::vec3 zAxis = rotation * glm::vec3(0.0f, 0.0f, 1.0f);\n    return glm::mat3(xAxis, yAxis, zAxis);\n}\n\nglm::mat3 MakeMat3(const glm::vec3& scale, const glm::quat& rotation)\n{\n    glm::vec3 xAxis = rotation * glm::vec3(scale.x, 0.0f, 0.0f);\n    glm::vec3 yAxis = rotation * glm::vec3(0.0f, scale.y, 0.0f);\n    glm::vec3 zAxis = rotation * glm::vec3(0.0f, 0.0f, scale.z);\n    return glm::mat3(xAxis, yAxis, zAxis);\n}\n\nglm::mat4 MakeMat3(float scale, const glm::quat& rotation)\n{\n    return MakeMat3(glm::vec3(scale), rotation);\n}\n\nglm::mat4 MakeMat4(const glm::vec3& scale, const glm::quat& rotation, const glm::vec3& translation)\n{\n    glm::vec3 xAxis = rotation * glm::vec3(scale.x, 0.0f, 0.0f);\n    glm::vec3 yAxis = rotation * glm::vec3(0.0f, scale.y, 0.0f);\n    glm::vec3 zAxis = rotation * glm::vec3(0.0f, 0.0f, scale.z);\n    return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f),\n                     glm::vec4(zAxis, 0.0f), glm::vec4(translation, 1.0f));\n}\n\nglm::mat4 MakeMat4(float scale, const glm::quat& rotation, const glm::vec3& translation)\n{\n    return MakeMat4(glm::vec3(scale), rotation, translation);\n}\n\nglm::mat4 MakeMat4(const glm::quat& rotation, const glm::vec3& translation)\n{\n    return MakeMat4(glm::vec3(1.0f), rotation, translation);\n}\n\nglm::mat4 MakeMat4(const glm::quat& rotation)\n{\n    return MakeMat4(glm::vec3(1.0f), rotation, glm::vec3(0.0f));\n}\n\nglm::mat4 MakeMat4(const glm::mat3& m3, const glm::vec3& translation)\n{\n    return glm::mat4(glm::vec4(m3[0], 0.0f), glm::vec4(m3[1], 0.0f),\n                     glm::vec4(m3[2], 0.0f), glm::vec4(translation, 1.0f));\n}\n\nglm::mat4 MakeMat4(const glm::mat3& m3)\n{\n    return MakeMat4(m3, glm::vec3(0.0f));\n}\n\nvoid Decompose(const glm::mat4& matrix, glm::vec3* scaleOut, glm::quat* rotationOut, glm::vec3* translationOut)\n{\n    glm::mat3 m(matrix);\n    float det = glm::determinant(m);\n    if (det < 0.0f)\n    {\n        // left handed matrix, flip sign to compensate.\n        *scaleOut = glm::vec3(-glm::length(m[0]), glm::length(m[1]), glm::length(m[2]));\n    }\n    else\n    {\n        *scaleOut = glm::vec3(glm::length(m[0]), glm::length(m[1]), glm::length(m[2]));\n    }\n\n    // quat_cast doesn't work so well with scaled matrices, so cancel it out.\n    glm::mat4 tmp = glm::scale(matrix, 1.0f / *scaleOut);\n    *rotationOut = glm::normalize(glm::quat_cast(tmp));\n\n    *translationOut = glm::vec3(matrix[3][0], matrix[3][1], matrix[3][2]);\n}\n\nvoid Decompose(const glm::mat3& matrix, glm::vec3* scaleOut, glm::quat* rotationOut)\n{\n    float det = glm::determinant(matrix);\n    if (det < 0.0f)\n    {\n        // left handed matrix, flip sign to compensate.\n        *scaleOut = glm::vec3(-glm::length(matrix[0]), glm::length(matrix[1]), glm::length(matrix[2]));\n    }\n    else\n    {\n        *scaleOut = glm::vec3(glm::length(matrix[0]), glm::length(matrix[1]), glm::length(matrix[2]));\n    }\n\n    // quat_cast doesn't work so well with scaled matrices, so cancel it out.\n    glm::mat3 tmp;\n    tmp[0] = matrix[0] * 1.0f / scaleOut->x;\n    tmp[1] = matrix[1] * 1.0f / scaleOut->y;\n    tmp[2] = matrix[2] * 1.0f / scaleOut->z;\n    *rotationOut = glm::normalize(glm::quat_cast(tmp));\n}\n\nvoid DecomposeSwingTwist(const glm::quat& rotation, const glm::vec3& twistAxis, glm::quat* swingOut, glm::quat* twistOut)\n{\n    glm::vec3 d = glm::normalize(twistAxis);\n\n    // the twist part has an axis (imaginary component) that is parallel to twistAxis argument\n    glm::vec3 axisOfRotation(rotation.x, rotation.y, rotation.z);\n    glm::vec3 twistImaginaryPart = glm::dot(d, axisOfRotation) * d;\n\n    // and a real component that is relatively proportional to rotation's real component\n    *twistOut = glm::normalize(glm::quat(rotation.w, twistImaginaryPart.x, twistImaginaryPart.y, twistImaginaryPart.z));\n\n    // once twist is known we can solve for swing:\n    // rotation = swing * twist  -->  swing = rotation * invTwist\n    *swingOut = rotation * glm::inverse(*twistOut);\n}\n\nglm::vec3 XformPoint(const glm::mat4& m, const glm::vec3& p)\n{\n    glm::vec4 temp(p, 1.0f);\n    glm::vec4 result = m * temp;\n    return glm::vec3(result.x / result.w, result.y / result.w, result.z / result.w);\n}\n\nglm::vec3 XformVec(const glm::mat4& m, const glm::vec3& v)\n{\n    return glm::mat3(m) * v;\n}\n\nglm::vec3 RandomColor()\n{\n    return glm::vec3(glm::linearRand(0.0f, 1.0f), glm::linearRand(0.0f, 1.0f), glm::linearRand(0.0f, 1.0f));\n}\n\nvoid PrintMat(const glm::mat4& m4, const std::string& name)\n{\n    Log::D(\"%s =\\n\", name.c_str());\n    Log::D(\"   | %10.5f, %10.5f, %10.5f, %10.5f |\\n\", m4[0][0], m4[1][0], m4[2][0], m4[3][0]);\n    Log::D(\"   | %10.5f, %10.5f, %10.5f, %10.5f |\\n\", m4[0][1], m4[1][1], m4[2][1], m4[3][1]);\n    Log::D(\"   | %10.5f, %10.5f, %10.5f, %10.5f |\\n\", m4[0][2], m4[1][2], m4[2][2], m4[3][2]);\n    Log::D(\"   | %10.5f, %10.5f, %10.5f, %10.5f |\\n\", m4[0][3], m4[1][3], m4[2][3], m4[3][3]);\n}\n\nvoid PrintMat(const glm::mat3& m3, const std::string& name)\n{\n    Log::D(\"%s =\\n\", name.c_str());\n    Log::D(\"   | %10.5f, %10.5f, %10.5f |\\n\", m3[0][0], m3[1][0], m3[2][0]);\n    Log::D(\"   | %10.5f, %10.5f, %10.5f |\\n\", m3[0][1], m3[1][1], m3[2][1]);\n    Log::D(\"   | %10.5f, %10.5f, %10.5f |\\n\", m3[0][2], m3[1][2], m3[2][2]);\n}\n\nvoid PrintMat(const glm::mat2& m2, const std::string& name)\n{\n    Log::D(\"%s =\\n\", name.c_str());\n    Log::D(\"   | %10.5f, %10.5f |\\n\", m2[0][0], m2[1][0]);\n    Log::D(\"   | %10.5f, %10.5f |\\n\", m2[0][1], m2[1][1]);\n}\n\nvoid PrintVec(const glm::vec4& v4, const std::string& name)\n{\n    Log::D(\"%s = ( %.5f, %.5f, %.5f, %.5f )\\n\", name.c_str(), v4.x, v4.y, v4.z, v4.w);\n}\n\nvoid PrintVec(const glm::vec3& v3, const std::string& name)\n{\n    Log::D(\"%s = ( %.5f, %.5f, %.5f )\\n\", name.c_str(), v3.x, v3.y, v3.z);\n}\n\nvoid PrintVec(const glm::vec2& v2, const std::string& name)\n{\n    Log::D(\"%s = ( %.5f, %.5f )\\n\", name.c_str(), v2.x, v2.y);\n}\n\nvoid PrintQuat(const glm::quat& q, const std::string& name)\n{\n    Log::D(\"%s = ( %.5f, ( %.5f, %.5f, %.5f ) )\\n\", name.c_str(), q.x, q.y, q.z, q.w);\n}\n\n#ifdef SHIPPING\nstatic std::string rootPath(\"\");\n#else\n#ifdef __linux__\n// enables us to run from the build dir\nstatic std::string rootPath(\"../\");\n#else\n// enables us to run from the build/Debug dir\nstatic std::string rootPath(\"../../\");\n#endif\n#endif\n\nconst std::string& GetRootPath()\n{\n    return rootPath;\n}\n\nvoid SetRootPath(const std::string& rootPathIn)\n{\n    rootPath = rootPathIn;\n}\n\nbool PointInsideAABB(const glm::vec3& point, const glm::vec3& aabbMin, const glm::vec3& aabbMax)\n{\n    return (point.x >= aabbMin.x && point.x <= aabbMax.x) &&\n           (point.y >= aabbMin.y && point.y <= aabbMax.y) &&\n           (point.z >= aabbMin.z && point.z <= aabbMax.z);\n}\n\nfloat LinearToSRGB(float linear)\n{\n    if (linear <= 0.0031308f)\n    {\n        return 12.92f * linear;\n    }\n    else\n    {\n        return 1.055f * glm::pow(linear, 1.0f / 2.4f) - 0.055f;\n    }\n}\n\nfloat SRGBToLinear(float srgb)\n{\n    if (srgb <= 0.04045f)\n    {\n        return srgb / 12.92f;\n    }\n    else\n    {\n        return glm::pow((srgb + 0.055f) / 1.055f, 2.4f);\n    }\n}\n\nglm::vec4 LinearToSRGB(const glm::vec4& linearColor)\n{\n    glm::vec4 sRGBColor;\n\n    for (int i = 0; i < 3; ++i)\n    {\n        sRGBColor[i] = LinearToSRGB(linearColor[i]);\n    }\n    sRGBColor.a = linearColor.a;\n    return sRGBColor;\n}\n\nglm::vec4 SRGBToLinear(const glm::vec4& srgbColor)\n{\n    glm::vec4 linearColor;\n    for (int i = 0; i < 3; ++i) // Convert RGB, leave A unchanged\n    {\n        linearColor[i] = SRGBToLinear(srgbColor[i]);\n    }\n    linearColor.a = srgbColor.a; // Copy alpha channel directly\n    return linearColor;\n}\n\nglm::mat4 MakeRotateAboutPointMat(const glm::vec3& pos, const glm::quat& rot)\n{\n    glm::mat4 posMat = MakeMat4(glm::quat(), pos);\n    glm::mat4 invPosMat = MakeMat4(glm::quat(), -pos);\n    glm::mat4 rotMat = MakeMat4(rot);\n    return posMat * rotMat * invPosMat;\n}\n\n// Creates a projection matrix based on the specified dimensions.\n// The projection matrix transforms -Z=forward, +Y=up, +X=right to the appropriate clip space for the graphics API.\n// The far plane is placed at infinity if farZ <= nearZ.\n// An infinite projection matrix is preferred for rasterization because, except for\n// things *right* up against the near plane, it always provides better precision:\n//              \"Tightening the Precision of Perspective Rendering\"\n//              Paul Upchurch, Mathieu Desbrun\n//              Journal of Graphics Tools, Volume 16, Issue 1, 2012\nvoid CreateProjection(float* m, GraphicsAPI graphicsApi, const float tanAngleLeft,\n                      const float tanAngleRight, const float tanAngleUp, float const tanAngleDown,\n                      const float nearZ, const float farZ)\n{\n    const float tanAngleWidth = tanAngleRight - tanAngleLeft;\n\n    // Set to tanAngleDown - tanAngleUp for a clip space with positive Y down (Vulkan).\n    // Set to tanAngleUp - tanAngleDown for a clip space with positive Y up (OpenGL / D3D / Metal).\n    const float tanAngleHeight = graphicsApi == GRAPHICS_VULKAN ? (tanAngleDown - tanAngleUp) : (tanAngleUp - tanAngleDown);\n\n    // Set to nearZ for a [-1,1] Z clip space (OpenGL / OpenGL ES).\n    // Set to zero for a [0,1] Z clip space (Vulkan / D3D / Metal).\n    const float offsetZ = (graphicsApi == GRAPHICS_OPENGL || graphicsApi == GRAPHICS_OPENGL_ES) ? nearZ : 0;\n\n    if (farZ <= nearZ)\n    {\n        // place the far plane at infinity\n        m[0] = 2 / tanAngleWidth;\n        m[4] = 0;\n        m[8] = (tanAngleRight + tanAngleLeft) / tanAngleWidth;\n        m[12] = 0;\n\n        m[1] = 0;\n        m[5] = 2 / tanAngleHeight;\n        m[9] = (tanAngleUp + tanAngleDown) / tanAngleHeight;\n        m[13] = 0;\n\n        m[2] = 0;\n        m[6] = 0;\n        m[10] = -1;\n        m[14] = -(nearZ + offsetZ);\n\n        m[3] = 0;\n        m[7] = 0;\n        m[11] = -1;\n        m[15] = 0;\n    }\n    else\n    {\n        // normal projection\n        m[0] = 2 / tanAngleWidth;\n        m[4] = 0;\n        m[8] = (tanAngleRight + tanAngleLeft) / tanAngleWidth;\n        m[12] = 0;\n\n        m[1] = 0;\n        m[5] = 2 / tanAngleHeight;\n        m[9] = (tanAngleUp + tanAngleDown) / tanAngleHeight;\n        m[13] = 0;\n\n        m[2] = 0;\n        m[6] = 0;\n        m[10] = -(farZ + offsetZ) / (farZ - nearZ);\n        m[14] = -(farZ * (nearZ + offsetZ)) / (farZ - nearZ);\n\n        m[3] = 0;\n        m[7] = 0;\n        m[11] = -1;\n        m[15] = 0;\n    }\n}\n\nvoid StrCpy_s(char* dest, size_t destsz, const char* src)\n{\n#ifdef WIN32\n    strcpy_s(dest, destsz, src);\n#else\n    strcpy(dest, src);\n#endif\n}\n"
  },
  {
    "path": "src/core/util.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n// helper functions\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <glm/gtc/quaternion.hpp>\n#include <string>\n\n// returns true on success, false on failure\nbool LoadFile(const std::string& filename, std::string& result);\nbool SaveFile(const std::string& filename, const std::string& data);\n\n// Iterate over codepoints in a utf-8 encoded string\nint NextCodePointUTF8(const char *str, uint32_t *codePointOut);\n\n#ifndef NDEBUG\n#define GL_ERROR_CHECK(x) GLErrorCheck(x)\nvoid GLErrorCheck(const char* message);\n#else\n#define GL_ERROR_CHECK(x)\n#endif\n\nglm::vec3 SafeNormalize(const glm::vec3& v, const glm::vec3& ifZero);\nglm::quat SafeMix(const glm::quat& a, const glm::quat& b, float alpha);\n\nglm::mat3 MakeMat3(const glm::quat& rotation);\nglm::mat3 MakeMat3(const glm::vec3& scale, const glm::quat& rotation);\nglm::mat4 MakeMat3(float scale, const glm::quat& rotation);\n\nglm::mat4 MakeMat4(const glm::vec3& scale, const glm::quat& rotation, const glm::vec3& translation);\nglm::mat4 MakeMat4(float scale, const glm::quat& rotation, const glm::vec3& translation);\nglm::mat4 MakeMat4(const glm::quat& rotation, const glm::vec3& translation);\nglm::mat4 MakeMat4(const glm::quat& rotation);\nglm::mat4 MakeMat4(const glm::mat3& m3, const glm::vec3& translation);\nglm::mat4 MakeMat4(const glm::mat3& m3);\n\nvoid Decompose(const glm::mat4& matrix, glm::vec3* scaleOut, glm::quat* rotationOut, glm::vec3* translationOut);\nvoid Decompose(const glm::mat3& matrix, glm::vec3* scaleOut, glm::quat* rotationOut);\nvoid DecomposeSwingTwist(const glm::quat& rotation, const glm::vec3& twistAxis, glm::quat* swingOut, glm::quat* twistOut);\n\nglm::vec3 XformPoint(const glm::mat4& m, const glm::vec3& p);\nglm::vec3 XformVec(const glm::mat4& m, const glm::vec3& v);\n\nglm::vec3 RandomColor();\n\nvoid PrintMat(const glm::mat4& m4, const std::string& name);\nvoid PrintMat(const glm::mat3& m3, const std::string& name);\nvoid PrintMat(const glm::mat2& m2, const std::string& name);\nvoid PrintVec(const glm::vec4& v4, const std::string& name);\nvoid PrintVec(const glm::vec3& v3, const std::string& name);\nvoid PrintVec(const glm::vec2& v2, const std::string& name);\nvoid PrintQuat(const glm::quat& q, const std::string& name);\n\nconst std::string& GetRootPath();\nvoid SetRootPath(const std::string& rootPathIn);\n\nbool PointInsideAABB(const glm::vec3& point, const glm::vec3& aabbMin, const glm::vec3& aabbMax);\nfloat LinearToSRGB(float linear);\nfloat SRGBToLinear(float srgb);\nglm::vec4 LinearToSRGB(const glm::vec4& linearColor);\nglm::vec4 SRGBToLinear(const glm::vec4& srgbColor);\n\nglm::mat4 MakeRotateAboutPointMat(const glm::vec3& pos, const glm::quat& rot);\n\nenum GraphicsAPI { GRAPHICS_VULKAN, GRAPHICS_OPENGL, GRAPHICS_OPENGL_ES, GRAPHICS_D3D };\nvoid CreateProjection(float* m, GraphicsAPI graphicsApi, const float tanAngleLeft,\n                      const float tanAngleRight, const float tanAngleUp, float const tanAngleDown,\n                      const float nearZ, const float farZ);\n\nvoid StrCpy_s(char* dest, size_t destsz, const char* src);\n"
  },
  {
    "path": "src/core/vertexbuffer.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"vertexbuffer.h\"\n\n#include <cassert>\n#include <string.h>\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#else\n#include <GL/glew.h>\n#define GL_GLEXT_PROTOTYPES 1\n#include <SDL2/SDL_opengl.h>\n#include <SDL2/SDL_opengl_glext.h>\n#endif\n\n#include \"util.h\"\n\n#ifdef __ANDROID__\nstatic void glBufferStorage(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags)\n{\n\tGLenum usage = 0;\n\tif (flags & GL_DYNAMIC_STORAGE_BIT)\n\t{\n\t\tif (flags & GL_MAP_READ_BIT)\n\t\t{\n\t\t\tusage = GL_DYNAMIC_READ;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tusage = GL_DYNAMIC_DRAW;\n\t\t}\n\t}\n\telse\n\t{\n\t\tif (flags & GL_MAP_READ_BIT)\n\t\t{\n\t\t\tusage = GL_STATIC_READ;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tusage = GL_STATIC_DRAW;\n\t\t}\n\t}\n\tglBufferData(target, size, data, usage);\n}\n#endif\n\nBufferObject::BufferObject(int targetIn, void* data, size_t size, unsigned int flags)\n{\n\ttarget = targetIn;\n    glGenBuffers(1, &obj);\n\tBind();\n    glBufferStorage(target, size, data, flags);\n\tUnbind();\n\telementSize = 0;\n\tnumElements = 0;\n}\n\nBufferObject::BufferObject(int targetIn, const std::vector<float>& data, unsigned int flags)\n{\n\ttarget = targetIn;\n    glGenBuffers(1, &obj);\n\tBind();\n    glBufferStorage(target, sizeof(float) * data.size(), (void*)data.data(), flags);\n\tUnbind();\n\telementSize = 1;\n\tnumElements = (int)data.size();\n}\n\nBufferObject::BufferObject(int targetIn, const std::vector<glm::vec2>& data, unsigned int flags)\n{\n\ttarget = targetIn;\n    glGenBuffers(1, &obj);\n\tBind();\n    glBufferStorage(target, sizeof(glm::vec2) * data.size(), (void*)data.data(), flags);\n\tUnbind();\n\telementSize = 2;\n\tnumElements = (int)data.size();\n}\n\nBufferObject::BufferObject(int targetIn, const std::vector<glm::vec3>& data, unsigned int flags)\n{\n\ttarget = targetIn;\n    glGenBuffers(1, &obj);\n\tBind();\n    glBufferStorage(target, sizeof(glm::vec3) * data.size(), (void*)data.data(), flags);\n\tUnbind();\n\telementSize = 3;\n\tnumElements = (int)data.size();\n}\n\nBufferObject::BufferObject(int targetIn, const std::vector<glm::vec4>& data, unsigned int flags)\n{\n\ttarget = targetIn;\n    glGenBuffers(1, &obj);\n\tBind();\n    glBufferStorage(target, sizeof(glm::vec4) * data.size(), (void*)data.data(), flags);\n\tUnbind();\n\telementSize = 4;\n\tnumElements = (int)data.size();\n}\n\nBufferObject::BufferObject(int targetIn, const std::vector<uint32_t>& data, unsigned int flags)\n{\n\ttarget = targetIn;\n    glGenBuffers(1, &obj);\n\tBind();\n    glBufferStorage(target, sizeof(uint32_t) * data.size(), (void*)data.data(), flags);\n\tUnbind();\n\telementSize = 1;\n\tnumElements = (int)data.size();\n}\n\nBufferObject::~BufferObject()\n{\n    glDeleteBuffers(1, &obj);\n}\n\nvoid BufferObject::Bind() const\n{\n\tglBindBuffer(target, obj);\n}\n\nvoid BufferObject::Unbind() const\n{\n\tglBindBuffer(target, 0);\n}\n\nvoid BufferObject::Update(const std::vector<float>& data)\n{\n\tBind();\n    glBufferSubData(target, 0, sizeof(float) * data.size(), (void*)data.data());\n\tUnbind();\n}\n\nvoid BufferObject::Update(const std::vector<glm::vec2>& data)\n{\n\tBind();\n    glBufferSubData(target, 0, sizeof(glm::vec2) * data.size(), (void*)data.data());\n\tUnbind();\n}\n\nvoid BufferObject::Update(const std::vector<glm::vec3>& data)\n{\n\tBind();\n    glBufferSubData(target, 0, sizeof(glm::vec3) * data.size(), (void*)data.data());\n\tUnbind();\n}\n\nvoid BufferObject::Update(const std::vector<glm::vec4>& data)\n{\n\tBind();\n    glBufferSubData(target, 0, sizeof(glm::vec4) * data.size(), (void*)data.data());\n\tUnbind();\n}\n\nvoid BufferObject::Update(const std::vector<uint32_t>& data)\n{\n\tBind();\n    glBufferSubData(target, 0, sizeof(uint32_t) * data.size(), (void*)data.data());\n\tUnbind();\n}\n\nvoid BufferObject::Read(std::vector<uint32_t>& data)\n{\n\tBind();\n\tsize_t bufferSize = sizeof(uint32_t) * data.size();\n\tassert(bufferSize == (elementSize * sizeof(uint32_t) * numElements));\n\t//void* rawBuffer = glMapBuffer(target, GL_READ_ONLY);\n\tvoid* rawBuffer = glMapBufferRange(target, 0, bufferSize, GL_MAP_READ_BIT);\n\tif (rawBuffer)\n\t{\n\t\tmemcpy((void*)data.data(), rawBuffer, bufferSize);\n\t}\n\tglUnmapBuffer(target);\n\tUnbind();\n}\n\nVertexArrayObject::VertexArrayObject()\n{\n\tglGenVertexArrays(1, &obj);\n}\n\nVertexArrayObject::~VertexArrayObject()\n{\n\tglDeleteVertexArrays(1, &obj);\n}\n\nvoid VertexArrayObject::Bind() const\n{\n\tglBindVertexArray(obj);\n}\n\nvoid VertexArrayObject::Unbind() const\n{\n\tglBindVertexArray(0);\n}\n\nvoid VertexArrayObject::SetAttribBuffer(int loc, std::shared_ptr<BufferObject> attribBuffer)\n{\n\tassert(attribBuffer->target == GL_ARRAY_BUFFER);\n\n\tBind();\n\tattribBuffer->Bind();\n\tglVertexAttribPointer(loc, attribBuffer->elementSize, GL_FLOAT, GL_FALSE, 0, nullptr);\n\tglEnableVertexAttribArray(loc);\n\tattribBuffer->Unbind();\n\tattribBufferVec.push_back(attribBuffer);\n\tUnbind();\n}\n\nvoid VertexArrayObject::SetElementBuffer(std::shared_ptr<BufferObject> elementBufferIn)\n{\n\tassert(elementBufferIn->target == GL_ELEMENT_ARRAY_BUFFER);\n\telementBuffer = elementBufferIn;\n\n\tBind();\n\telementBufferIn->Bind();\n\tUnbind();\n}\n\nvoid VertexArrayObject::DrawElements(int mode) const\n{\n\tBind();\n\tglDrawElements((GLenum)mode, elementBuffer->numElements, GL_UNSIGNED_INT, nullptr);\n\tUnbind();\n}\n\n"
  },
  {
    "path": "src/core/vertexbuffer.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <memory>\n#include <stdint.h>\n#include <vector>\n\nclass VertexArrayObject;\n\n#ifdef __ANDROID__\n// AJT: ANDROID: TODO: HACK TO WORK AROUND glBufferStorage\n#define GL_DYNAMIC_STORAGE_BIT            0x0100\n#define GL_MAP_READ_BIT                   0x0001\n#endif\n\nclass BufferObject\n{\n\tfriend class VertexArrayObject;\npublic:\n\n\t// targetIn should be one of the following.\n\t//     GL_ARRAY_BUFFER, GL_ATOMIC_COUNTER_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER\n\t//     GL_DISPATCH_INDIRECT_BUFFER, GL_DRAW_INDIRECT_BUFFER, GL_ELEMENT_ARRAY_BUFFER,\n\t//     GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_QUERY_BUFFER, GL_SHADER_STORAGE_BUFFER,\n\t//     GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, GL_UNIFORM_BUFFER\n\t// flags can one of the following bitfields.\n\t//     GL_DYNAMIC_STORAGE_BIT, GL_MAP_READ_BIT, GL_MAP_WRITE_BIT, GL_MAP_PERSISTENT_BIT\n\t//     GL_MAP_COHERENT_BIT, GL_CLIENT_STORAGE_BIT\n\tBufferObject(int targetIn, void* data, size_t size, unsigned int flags = 0);\n    BufferObject(int targetIn, const std::vector<float>& data, unsigned int flags = 0);\n\tBufferObject(int targetIn, const std::vector<glm::vec2>& data, unsigned int flags = 0);\n\tBufferObject(int targetIn, const std::vector<glm::vec3>& data, unsigned int flags = 0);\n\tBufferObject(int targetIn, const std::vector<glm::vec4>& data, unsigned int flags = 0);\n\tBufferObject(int targetIn, const std::vector<uint32_t>& data, unsigned int flags = 0);\n\tBufferObject(const BufferObject& orig) = delete;\n    ~BufferObject();\n\n\tvoid Bind() const;\n\tvoid Unbind() const;\n\n\tvoid Update(const std::vector<float>& data);\n\tvoid Update(const std::vector<glm::vec2>& data);\n\tvoid Update(const std::vector<glm::vec3>& data);\n\tvoid Update(const std::vector<glm::vec4>& data);\n\tvoid Update(const std::vector<uint32_t>& data);\n\n\tvoid Read(std::vector<uint32_t>& data);\n\n\tuint32_t GetObj() const { return obj; }\n\nprotected:\n\tint target;\n    uint32_t obj;\n\tint elementSize;  // vec2 = 2, vec3 = 3 etc.\n\tint numElements;  // number of vec2, vec3 in buffer\n};\n\nclass VertexArrayObject\n{\npublic:\n\tVertexArrayObject();\n\t~VertexArrayObject();\n\n\tvoid Bind() const;\n\tvoid Unbind() const;\n\n\tvoid SetAttribBuffer(int loc, std::shared_ptr<BufferObject> attribBufferIn);\n\tvoid SetElementBuffer(std::shared_ptr<BufferObject> elementBufferIn);\n\tstd::shared_ptr<BufferObject> GetElementBuffer() const { return elementBuffer; }\n\tvoid DrawElements(int mode) const;\n\nprotected:\n\tuint32_t obj;\n\tstd::vector<std::shared_ptr<BufferObject>> attribBufferVec;\n\tstd::shared_ptr<BufferObject> elementBuffer;\n};\n"
  },
  {
    "path": "src/core/xrbuddy.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"xrbuddy.h\"\n\n#include <cassert>\n#include <string.h>\n#include <vector>\n\n#ifndef __ANDROID__\n#include <GL/glew.h>\n#endif\n\n#include <glm/gtc/type_ptr.hpp>\n\n#ifdef TRACY_ENABLE\n#include <tracy/Tracy.hpp>\n#else\n#define ZoneScoped\n#define ZoneScopedNC(NAME, COLOR)\n#endif\n\n#include \"log.h\"\n#include \"util.h\"\n\nstatic bool printAll = true;\n\nstruct PathCache\n{\n    PathCache(XrInstance instanceIn) : instance(instanceIn) {}\n    XrPath operator[](const std::string& key)\n    {\n        auto iter = pathMap.find(key);\n        if (iter != pathMap.end())\n        {\n            return iter->second;\n        }\n        else\n        {\n            XrPath path = XR_NULL_PATH;\n            xrStringToPath(instance, key.c_str(), &path);\n            pathMap.insert({ key, path });\n            return path;\n        }\n    }\nprotected:\n    XrInstance instance;\n    std::map<std::string, XrPath> pathMap;\n};\n\nstatic bool CheckResult(XrInstance instance, XrResult result, const char* str)\n{\n    if (XR_SUCCEEDED(result))\n    {\n        return true;\n    }\n\n    if (instance != XR_NULL_HANDLE)\n    {\n        char resultString[XR_MAX_RESULT_STRING_SIZE];\n        xrResultToString(instance, result, resultString);\n        Log::E(\"%s [%s]\\n\", str, resultString);\n    }\n    else\n    {\n        Log::E(\"%s\\n\", str);\n    }\n    return false;\n}\n\nstatic bool EnumerateExtensions(std::vector<XrExtensionProperties>& extensionProps)\n{\n    XrResult result;\n\n    uint32_t extensionCount = 0;\n    result = xrEnumerateInstanceExtensionProperties(NULL, 0, &extensionCount, NULL);\n    if (!CheckResult(NULL, result, \"xrEnumerateInstanceExtensionProperties failed\"))\n    {\n        return false;\n    }\n\n    extensionProps.resize(extensionCount);\n    for (uint32_t i = 0; i < extensionCount; i++)\n    {\n        extensionProps[i].type = XR_TYPE_EXTENSION_PROPERTIES;\n        extensionProps[i].next = NULL;\n    }\n    result = xrEnumerateInstanceExtensionProperties(NULL, extensionCount, &extensionCount, extensionProps.data());\n    if (!CheckResult(NULL, result, \"xrEnumerateInstanceExtensionProperties failed\"))\n    {\n        return false;\n    }\n\n    bool printExtensions = false;\n    if (printExtensions || printAll)\n    {\n        Log::D(\"%d extensions:\\n\", extensionCount);\n        for (uint32_t i = 0; i < extensionCount; ++i)\n        {\n            Log::D(\"    %s\\n\", extensionProps[i].extensionName);\n        }\n    }\n\n    return true;\n}\n\nstatic bool ExtensionSupported(const std::vector<XrExtensionProperties>& extensions, const char* extensionName)\n{\n    bool supported = false;\n    for (auto& extension : extensions)\n    {\n        if (!strcmp(extensionName, extension.extensionName))\n        {\n            supported = true;\n        }\n    }\n    return supported;\n}\n\nstatic bool EnumerateApiLayers(std::vector<XrApiLayerProperties>& layerProps)\n{\n    uint32_t layerCount;\n    XrResult result = xrEnumerateApiLayerProperties(0, &layerCount, NULL);\n    if (!CheckResult(NULL, result, \"xrEnumerateApiLayerProperties\"))\n    {\n        return false;\n    }\n\n    layerProps.resize(layerCount);\n    for (uint32_t i = 0; i < layerCount; i++)\n    {\n        layerProps[i].type = XR_TYPE_API_LAYER_PROPERTIES;\n        layerProps[i].next = NULL;\n    }\n    result = xrEnumerateApiLayerProperties(layerCount, &layerCount, layerProps.data());\n    if (!CheckResult(NULL, result, \"xrEnumerateApiLayerProperties\"))\n    {\n        return false;\n    }\n\n    bool printLayers = false;\n    if (printLayers || printAll)\n    {\n        Log::D(\"%d XrApiLayerProperties:\\n\", layerCount);\n        for (uint32_t i = 0; i < layerCount; i++)\n        {\n            Log::D(\"    %s, %s\\n\", layerProps[i].layerName, layerProps[i].description);\n        }\n    }\n\n    return true;\n}\n\nstatic bool CreateInstance(XrInstance& instance, const std::vector<const char*>& extensionVec)\n{\n    if (printAll)\n    {\n        Log::D(\"Attempting to enable the following extensions:\\n\");\n        for (auto&& ext : extensionVec)\n        {\n            Log::D(\"    %s\\n\", ext);\n        }\n    }\n    // create openxr instance\n    XrResult result;\n    XrInstanceCreateInfo ici = {};\n    ici.type = XR_TYPE_INSTANCE_CREATE_INFO;\n    ici.next = NULL;\n    ici.createFlags = 0;\n    ici.enabledExtensionCount = (uint32_t)extensionVec.size();\n    ici.enabledExtensionNames = extensionVec.data();\n    ici.enabledApiLayerCount = 0;\n    ici.enabledApiLayerNames = NULL;\n    StrCpy_s(ici.applicationInfo.applicationName, XR_MAX_APPLICATION_NAME_SIZE, \"xrtoy\");\n    ici.applicationInfo.engineName[0] = '\\0';\n    ici.applicationInfo.applicationVersion = 1;\n    ici.applicationInfo.engineVersion = 0;\n    ici.applicationInfo.apiVersion = XR_CURRENT_API_VERSION;\n\n    result = xrCreateInstance(&ici, &instance);\n    if (!CheckResult(NULL, result, \"xrCreateInstance failed\"))\n    {\n        return false;\n    }\n\n    bool printRuntimeInfo = false;\n    if (printRuntimeInfo || printAll)\n    {\n        XrInstanceProperties ip = {};\n        ip.type = XR_TYPE_INSTANCE_PROPERTIES;\n        ip.next = NULL;\n\n        result = xrGetInstanceProperties(instance, &ip);\n        if (!CheckResult(instance, result, \"xrGetInstanceProperties failed\"))\n        {\n            return false;\n        }\n\n        Log::D(\"Runtime Name: %s\\n\", ip.runtimeName);\n        Log::D(\"Runtime Version: %d.%d.%d\\n\",\n               XR_VERSION_MAJOR(ip.runtimeVersion),\n               XR_VERSION_MINOR(ip.runtimeVersion),\n               XR_VERSION_PATCH(ip.runtimeVersion));\n    }\n\n    return true;\n}\n\nstatic bool GetSystemId(XrInstance instance, XrSystemId& systemId)\n{\n    XrResult result;\n    XrSystemGetInfo sgi = {};\n    sgi.type = XR_TYPE_SYSTEM_GET_INFO;\n    sgi.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;\n    sgi.next = NULL;\n\n    result = xrGetSystem(instance, &sgi, &systemId);\n    if (!CheckResult(instance, result, \"xrGetSystemFailed\"))\n    {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool GetSystemProperties(XrInstance instance, XrSystemId systemId, XrSystemProperties& sp)\n{\n    XrResult result;\n    sp.type = XR_TYPE_SYSTEM_PROPERTIES;\n    sp.next = NULL;\n    sp.graphicsProperties = {0};\n    sp.trackingProperties = {0};\n\n    result = xrGetSystemProperties(instance, systemId, &sp);\n    if (!CheckResult(instance, result, \"xrGetSystemProperties failed\"))\n    {\n        return false;\n    }\n\n    bool printSystemProperties = false;\n    if (printSystemProperties || printAll)\n    {\n        Log::D(\"System properties for system \\\"%s\\\":\\n\", sp.systemName);\n        Log::D(\"    maxLayerCount: %d\\n\", sp.graphicsProperties.maxLayerCount);\n        Log::D(\"    maxSwapChainImageHeight: %d\\n\", sp.graphicsProperties.maxSwapchainImageHeight);\n        Log::D(\"    maxSwapChainImageWidth: %d\\n\", sp.graphicsProperties.maxSwapchainImageWidth);\n        Log::D(\"    Orientation Tracking: %s\\n\", sp.trackingProperties.orientationTracking ? \"true\" : \"false\");\n        Log::D(\"    Position Tracking: %s\\n\", sp.trackingProperties.positionTracking ? \"true\" : \"false\");\n    }\n\n    return true;\n}\n\nstatic bool SupportsVR(XrInstance instance, XrSystemId systemId)\n{\n    XrResult result;\n    uint32_t viewConfigurationCount;\n    result = xrEnumerateViewConfigurations(instance, systemId, 0, &viewConfigurationCount, NULL);\n    if (!CheckResult(instance, result, \"xrEnumerateViewConfigurations\"))\n    {\n        return false;\n    }\n\n    std::vector<XrViewConfigurationType> viewConfigurations(viewConfigurationCount);\n    result = xrEnumerateViewConfigurations(instance, systemId, viewConfigurationCount, &viewConfigurationCount, viewConfigurations.data());\n    if (!CheckResult(instance, result, \"xrEnumerateViewConfigurations\"))\n    {\n        return false;\n    }\n\n    XrViewConfigurationType stereoViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;\n    for (uint32_t i = 0; i < viewConfigurationCount; i++)\n    {\n        XrViewConfigurationProperties vcp = {};\n        vcp.type = XR_TYPE_VIEW_CONFIGURATION_PROPERTIES;\n        vcp.next = NULL;\n\n        result = xrGetViewConfigurationProperties(instance, systemId, viewConfigurations[i], &vcp);\n        if (!CheckResult(instance, result, \"xrGetViewConfigurationProperties\"))\n        {\n            return false;\n        }\n\n        if (viewConfigurations[i] == stereoViewConfigType && vcp.viewConfigurationType == stereoViewConfigType)\n        {\n            return true;\n        }\n    }\n\n    return false;\n}\n\nstatic bool EnumerateViewConfigs(XrInstance instance, XrSystemId systemId, std::vector<XrViewConfigurationView>& viewConfigs)\n{\n    XrResult result;\n    uint32_t viewCount;\n    XrViewConfigurationType stereoViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;\n    result = xrEnumerateViewConfigurationViews(instance, systemId, stereoViewConfigType, 0, &viewCount, NULL);\n    if (!CheckResult(instance, result, \"xrEnumerateViewConfigurationViews\"))\n    {\n        return false;\n    }\n\n    viewConfigs.resize(viewCount);\n    for (uint32_t i = 0; i < viewCount; i++)\n    {\n        viewConfigs[i].type = XR_TYPE_VIEW_CONFIGURATION_VIEW;\n        viewConfigs[i].next = NULL;\n    }\n\n    result = xrEnumerateViewConfigurationViews(instance, systemId, stereoViewConfigType, viewCount, &viewCount, viewConfigs.data());\n    if (!CheckResult(instance, result, \"xrEnumerateViewConfigurationViews\"))\n    {\n        return false;\n    }\n\n    bool printViews = false;\n    if (printViews || printAll)\n    {\n        Log::D(\"%d viewConfigs:\\n\", viewCount);\n        for (uint32_t i = 0; i < viewCount; i++)\n        {\n            Log::D(\"    viewConfigs[%d]:\\n\", i);\n            Log::D(\"        recommendedImageRectWidth: %d\\n\", viewConfigs[i].recommendedImageRectWidth);\n            Log::D(\"        maxImageRectWidth: %d\\n\", viewConfigs[i].maxImageRectWidth);\n            Log::D(\"        recommendedImageRectHeight: %d\\n\", viewConfigs[i].recommendedImageRectHeight);\n            Log::D(\"        maxImageRectHeight: %d\\n\", viewConfigs[i].maxImageRectHeight);\n            Log::D(\"        recommendedSwapchainSampleCount: %d\\n\", viewConfigs[i].recommendedSwapchainSampleCount);\n            Log::D(\"        maxSwapchainSampleCount: %d\\n\", viewConfigs[i].maxSwapchainSampleCount);\n        }\n    }\n\n    return true;\n}\n\nstatic bool SetColorSpace(XrInstance instance, XrSession session, XrColorSpaceFB colorSpace)\n{\n    XrResult result;\n\n    PFN_xrSetColorSpaceFB xrSetColorSpaceFB = nullptr;\n    result = xrGetInstanceProcAddr(instance, \"xrSetColorSpaceFB\", (PFN_xrVoidFunction*)&xrSetColorSpaceFB);\n    if (!CheckResult(instance, result, \"xrGetInstanceProcAddr(xrSetColorSpaceFB)\") || !xrSetColorSpaceFB)\n    {\n        Log::W(\"get proc addr xrSetColorSpaceFB() failed\\n\");\n        return false;\n    }\n\n    result = xrSetColorSpaceFB(session, colorSpace);\n    if (!CheckResult(instance, result, \"xrSetColorSpaceFB\"))\n    {\n        Log::W(\"xrSetColorSpaceFB(XR_COLOR_SPACE_REC2020_FB) failed\\n\");\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool CreateSession(XrInstance instance, XrSystemId systemId, XrSession& session, const MainContext& mainContext)\n{\n    XrResult result;\n\n    // check if opengl version is sufficient.\n#ifdef XR_USE_GRAPHICS_API_OPENGL\n    {\n        XrGraphicsRequirementsOpenGLKHR reqs = {};\n        reqs.type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR;\n        reqs.next = NULL;\n        PFN_xrGetOpenGLGraphicsRequirementsKHR pfnGetOpenGLGraphicsRequirementsKHR = NULL;\n        result = xrGetInstanceProcAddr(instance, \"xrGetOpenGLGraphicsRequirementsKHR\", (PFN_xrVoidFunction *)&pfnGetOpenGLGraphicsRequirementsKHR);\n        if (!CheckResult(instance, result, \"xrGetInstanceProcAddr\"))\n        {\n            return false;\n        }\n\n        result = pfnGetOpenGLGraphicsRequirementsKHR(instance, systemId, &reqs);\n        if (!CheckResult(instance, result, \"GetOpenGLGraphicsRequirementsPKR\"))\n        {\n            return false;\n        }\n\n        GLint major = 0;\n        GLint minor = 0;\n        glGetIntegerv(GL_MAJOR_VERSION, &major);\n        glGetIntegerv(GL_MINOR_VERSION, &minor);\n        const XrVersion desiredApiVersion = XR_MAKE_VERSION(major, minor, 0);\n\n        bool printVersion = false;\n        if (printVersion || printAll)\n        {\n            Log::D(\"current OpenGL version: %d.%d.%d\\n\", XR_VERSION_MAJOR(desiredApiVersion),\n                   XR_VERSION_MINOR(desiredApiVersion), XR_VERSION_PATCH(desiredApiVersion));\n            Log::D(\"minimum OpenGL version: %d.%d.%d\\n\", XR_VERSION_MAJOR(reqs.minApiVersionSupported),\n                   XR_VERSION_MINOR(reqs.minApiVersionSupported), XR_VERSION_PATCH(reqs.minApiVersionSupported));\n        }\n\n        if (reqs.minApiVersionSupported > desiredApiVersion)\n        {\n            Log::E(\"Runtime does not support desired Graphics API and/or version\\n\");\n            return false;\n        }\n    }\n\n#ifdef WIN32\n    XrGraphicsBindingOpenGLWin32KHR glBinding = {};\n    glBinding.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR;\n    glBinding.next = NULL;\n    glBinding.hDC = wglGetCurrentDC();\n    glBinding.hGLRC = wglGetCurrentContext();\n#else\n    XrGraphicsBindingOpenGLXlibKHR glBinding = {};\n    glBinding.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR;\n    glBinding.next = NULL;\n    glBinding.xDisplay = mainContext.xdisplay;\n    glBinding.glxDrawable = mainContext.glxDrawable;\n    glBinding.glxContext = mainContext.glxContext;\n#endif\n\n#elif defined (XR_USE_GRAPHICS_API_OPENGL_ES)\n    XrGraphicsRequirementsOpenGLESKHR reqs = {};\n    reqs.type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR;\n    reqs.next = NULL;\n    PFN_xrGetOpenGLESGraphicsRequirementsKHR pfnGetOpenGLESGraphicsRequirementsKHR = NULL;\n    result = xrGetInstanceProcAddr(instance, \"xrGetOpenGLESGraphicsRequirementsKHR\", (PFN_xrVoidFunction *)&pfnGetOpenGLESGraphicsRequirementsKHR);\n    if (!CheckResult(instance, result, \"xrGetInstanceProcAddr\"))\n    {\n        return false;\n    }\n\n    result = pfnGetOpenGLESGraphicsRequirementsKHR(instance, systemId, &reqs);\n    if (!CheckResult(instance, result, \"GetOpenGLGraphicsRequirementsPKR\"))\n    {\n        return false;\n    }\n\n    GLint major = 0;\n    GLint minor = 0;\n    glGetIntegerv(GL_MAJOR_VERSION, &major);\n    glGetIntegerv(GL_MINOR_VERSION, &minor);\n    const XrVersion desiredApiVersion = XR_MAKE_VERSION(major, minor, 0);\n\n    bool printVersion = false;\n    if (printVersion || printAll)\n    {\n        Log::D(\"current OpenGLES version: %d.%d.%d\\n\", XR_VERSION_MAJOR(desiredApiVersion),\n               XR_VERSION_MINOR(desiredApiVersion), XR_VERSION_PATCH(desiredApiVersion));\n        Log::D(\"minimum OpenGLES version: %d.%d.%d\\n\", XR_VERSION_MAJOR(reqs.minApiVersionSupported),\n               XR_VERSION_MINOR(reqs.minApiVersionSupported), XR_VERSION_PATCH(reqs.minApiVersionSupported));\n    }\n\n    if (reqs.minApiVersionSupported > desiredApiVersion)\n    {\n        Log::E(\"Runtime does not support desired Graphics API and/or version\\n\");\n        return false;\n    }\n\n    XrGraphicsBindingOpenGLESAndroidKHR glBinding = {};\n    glBinding.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR;\n    glBinding.next = NULL;\n    glBinding.display = mainContext.display;\n    glBinding.config = mainContext.config;\n    glBinding.context = mainContext.context;\n\n#endif\n\n    XrSessionCreateInfo sci = {};\n    sci.type = XR_TYPE_SESSION_CREATE_INFO;\n    sci.next = &glBinding;\n    sci.createFlags = 0;\n    sci.systemId = systemId;\n\n    result = xrCreateSession(instance, &sci, &session);\n    if (!CheckResult(instance, result, \"xrCreateSession\"))\n    {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool CreateActions(XrInstance instance, XrSystemId systemId, XrSession session, XrActionSet& actionSet,\n                          std::map<std::string, XrBuddy::ActionInfo>& actionMap)\n{\n    XrResult result;\n\n    // create action set\n    XrActionSetCreateInfo asci = {};\n    asci.type = XR_TYPE_ACTION_SET_CREATE_INFO;\n    asci.next = NULL;\n    StrCpy_s(asci.actionSetName, XR_MAX_ACTION_NAME_SIZE, \"default\");\n    StrCpy_s(asci.localizedActionSetName, XR_MAX_LOCALIZED_ACTION_NAME_SIZE, \"Default\");\n    asci.priority = 0;\n    result = xrCreateActionSet(instance, &asci, &actionSet);\n    if (!CheckResult(instance, result, \"xrCreateActionSet\"))\n    {\n        return false;\n    }\n\n    std::vector<std::pair<std::string, XrActionType>> actionPairVec = {\n        {\"l_select_click\", XR_ACTION_TYPE_BOOLEAN_INPUT},\n        {\"r_select_click\", XR_ACTION_TYPE_BOOLEAN_INPUT},\n        {\"l_menu_click\", XR_ACTION_TYPE_BOOLEAN_INPUT},\n        {\"r_menu_click\", XR_ACTION_TYPE_BOOLEAN_INPUT},\n        {\"l_squeeze_click\", XR_ACTION_TYPE_BOOLEAN_INPUT},\n        {\"r_squeeze_click\", XR_ACTION_TYPE_BOOLEAN_INPUT},\n        {\"l_trackpad_click\", XR_ACTION_TYPE_BOOLEAN_INPUT},\n        {\"r_trackpad_click\", XR_ACTION_TYPE_BOOLEAN_INPUT},\n        {\"l_trackpad_x\", XR_ACTION_TYPE_FLOAT_INPUT},\n        {\"r_trackpad_x\", XR_ACTION_TYPE_FLOAT_INPUT},\n        {\"l_trackpad_y\", XR_ACTION_TYPE_FLOAT_INPUT},\n        {\"r_trackpad_y\", XR_ACTION_TYPE_FLOAT_INPUT},\n\n        {\"l_grip_pose\", XR_ACTION_TYPE_POSE_INPUT},\n        {\"r_grip_pose\", XR_ACTION_TYPE_POSE_INPUT},\n        {\"l_aim_pose\", XR_ACTION_TYPE_POSE_INPUT},\n        {\"r_aim_pose\", XR_ACTION_TYPE_POSE_INPUT},\n\n        {\"l_haptic\", XR_ACTION_TYPE_VIBRATION_OUTPUT},\n        {\"r_haptic\", XR_ACTION_TYPE_VIBRATION_OUTPUT},\n\n        {\"l_stick\", XR_ACTION_TYPE_VECTOR2F_INPUT},\n        {\"r_stick\", XR_ACTION_TYPE_VECTOR2F_INPUT}\n    };\n\n    for (auto& actionPair : actionPairVec)\n    {\n        // selectAction\n        XrAction action = XR_NULL_HANDLE;\n        XrActionCreateInfo aci = {};\n        aci.type = XR_TYPE_ACTION_CREATE_INFO;\n        aci.next = NULL;\n        aci.actionType = actionPair.second;\n        StrCpy_s(aci.actionName, XR_MAX_ACTION_NAME_SIZE, actionPair.first.c_str());\n        StrCpy_s(aci.localizedActionName, XR_MAX_LOCALIZED_ACTION_NAME_SIZE, actionPair.first.c_str());\n        aci.countSubactionPaths = 0;\n        aci.subactionPaths = NULL;\n        result = xrCreateAction(actionSet, &aci, &action);\n        if (!CheckResult(instance, result, \"xrCreateAction\"))\n        {\n            return false;\n        }\n\n        XrSpace space = XR_NULL_HANDLE;\n        if (actionPair.second == XR_ACTION_TYPE_POSE_INPUT)\n        {\n            XrActionSpaceCreateInfo aspci = {};\n            aspci.type = XR_TYPE_ACTION_SPACE_CREATE_INFO;\n            aspci.next = NULL;\n            aspci.action = action;\n            XrPosef identity;\n            identity.orientation = {0.0f, 0.0f, 0.0f, 1.0f};\n            identity.position = {0.0f, 0.0f, 0.0f};\n            aspci.poseInActionSpace = identity;\n            aspci.subactionPath = 0;\n            result = xrCreateActionSpace(session, &aspci, &space);\n            if (!CheckResult(instance, result, \"xrCreateActionSpace\"))\n            {\n                return false;\n            }\n        }\n        actionMap[actionPair.first] = XrBuddy::ActionInfo(action, actionPair.second, space);\n    }\n\n    PathCache pathCache(instance);\n    if (!CheckResult(instance, result, \"xrStringToPath\"))\n    {\n        return false;\n    }\n\n    // KHR Simple\n    {\n        XrPath interactionProfilePath = XR_NULL_PATH;\n        xrStringToPath(instance, \"/interaction_profiles/khr/simple_controller\", &interactionProfilePath);\n        std::vector<XrActionSuggestedBinding> bindings = {\n            {actionMap[\"l_select_click\"].action, pathCache[\"/user/hand/left/input/select/click\"]},\n            {actionMap[\"r_select_click\"].action, pathCache[\"/user/hand/right/input/select/click\"]},\n            {actionMap[\"l_menu_click\"].action, pathCache[\"/user/hand/left/input/menu/click\"]},\n            {actionMap[\"r_menu_click\"].action, pathCache[\"/user/hand/right/input/menu/click\"]},\n            {actionMap[\"l_grip_pose\"].action, pathCache[\"/user/hand/left/input/grip/pose\"]},\n            {actionMap[\"r_grip_pose\"].action, pathCache[\"/user/hand/right/input/grip/pose\"]},\n            {actionMap[\"l_aim_pose\"].action, pathCache[\"/user/hand/left/input/aim/pose\"]},\n            {actionMap[\"r_aim_pose\"].action, pathCache[\"/user/hand/right/input/aim/pose\"]},\n            {actionMap[\"l_haptic\"].action, pathCache[\"/user/hand/left/output/haptic\"]},\n            {actionMap[\"r_haptic\"].action, pathCache[\"/user/hand/right/output/haptic\"]}\n        };\n\n        XrInteractionProfileSuggestedBinding suggestedBindings = {};\n        suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING;\n        suggestedBindings.next = NULL;\n        suggestedBindings.interactionProfile = interactionProfilePath;\n        suggestedBindings.suggestedBindings = bindings.data();\n        suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size();\n        result = xrSuggestInteractionProfileBindings(instance, &suggestedBindings);\n        if (!CheckResult(instance, result, \"xrSuggestInteractionProfileBindings\"))\n        {\n            return false;\n        }\n    }\n\n    // oculus touch\n    {\n        XrPath interactionProfilePath = XR_NULL_PATH;\n        xrStringToPath(instance, \"/interaction_profiles/oculus/touch_controller\", &interactionProfilePath);\n        std::vector<XrActionSuggestedBinding> bindings = {\n            {actionMap[\"l_select_click\"].action, pathCache[\"/user/hand/left/input/trigger/value\"]},\n            {actionMap[\"r_select_click\"].action, pathCache[\"/user/hand/right/input/trigger/value\"]},\n            {actionMap[\"l_menu_click\"].action, pathCache[\"/user/hand/left/input/menu/click\"]},\n            //{actionMap[\"r_menu_click\"].action, pathCache[\"/user/hand/right/input/menu/click\"]},  // right controller has no menu button\n            {actionMap[\"l_squeeze_click\"].action, pathCache[\"/user/hand/left/input/squeeze/value\"]},\n            {actionMap[\"r_squeeze_click\"].action, pathCache[\"/user/hand/right/input/squeeze/value\"]},\n\n            {actionMap[\"l_grip_pose\"].action, pathCache[\"/user/hand/left/input/grip/pose\"]},\n            {actionMap[\"r_grip_pose\"].action, pathCache[\"/user/hand/right/input/grip/pose\"]},\n            {actionMap[\"l_aim_pose\"].action, pathCache[\"/user/hand/left/input/aim/pose\"]},\n            {actionMap[\"r_aim_pose\"].action, pathCache[\"/user/hand/right/input/aim/pose\"]},\n            {actionMap[\"l_haptic\"].action, pathCache[\"/user/hand/left/output/haptic\"]},\n            {actionMap[\"r_haptic\"].action, pathCache[\"/user/hand/right/output/haptic\"]},\n\n            {actionMap[\"l_stick\"].action, pathCache[\"/user/hand/left/input/thumbstick\"]},\n            {actionMap[\"r_stick\"].action, pathCache[\"/user/hand/right/input/thumbstick\"]}\n        };\n\n        XrInteractionProfileSuggestedBinding suggestedBindings = {};\n        suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING;\n        suggestedBindings.next = NULL;\n        suggestedBindings.interactionProfile = interactionProfilePath;\n        suggestedBindings.suggestedBindings = bindings.data();\n        suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size();\n        result = xrSuggestInteractionProfileBindings(instance, &suggestedBindings);\n        if (!CheckResult(instance, result, \"xrSuggestInteractionProfileBindings (oculus)\"))\n        {\n            return false;\n        }\n    }\n\n    // vive\n    {\n        XrPath interactionProfilePath = XR_NULL_PATH;\n        xrStringToPath(instance, \"/interaction_profiles/htc/vive_controller\", &interactionProfilePath);\n        std::vector<XrActionSuggestedBinding> bindings = {\n            {actionMap[\"l_menu_click\"].action, pathCache[\"/user/hand/left/input/menu/click\"]},\n            {actionMap[\"r_menu_click\"].action, pathCache[\"/user/hand/right/input/menu/click\"]},\n\n            {actionMap[\"l_select_click\"].action, pathCache[\"/user/hand/left/input/trigger/click\"]},\n            {actionMap[\"r_select_click\"].action, pathCache[\"/user/hand/right/input/trigger/click\"]},\n\n            {actionMap[\"l_squeeze_click\"].action, pathCache[\"/user/hand/left/input/squeeze/click\"]},\n            {actionMap[\"r_squeeze_click\"].action, pathCache[\"/user/hand/right/input/squeeze/click\"]},\n\n            {actionMap[\"l_trackpad_click\"].action, pathCache[\"/user/hand/left/input/trackpad/click\"]},\n            {actionMap[\"r_trackpad_click\"].action, pathCache[\"/user/hand/right/input/trackpad/click\"]},\n\n            {actionMap[\"l_trackpad_x\"].action, pathCache[\"/user/hand/left/input/trackpad/x\"]},\n            {actionMap[\"r_trackpad_x\"].action, pathCache[\"/user/hand/right/input/trackpad/x\"]},\n            {actionMap[\"l_trackpad_y\"].action, pathCache[\"/user/hand/left/input/trackpad/y\"]},\n            {actionMap[\"r_trackpad_y\"].action, pathCache[\"/user/hand/right/input/trackpad/y\"]},\n\n            {actionMap[\"l_grip_pose\"].action, pathCache[\"/user/hand/left/input/grip/pose\"]},\n            {actionMap[\"r_grip_pose\"].action, pathCache[\"/user/hand/right/input/grip/pose\"]},\n\n            {actionMap[\"l_aim_pose\"].action, pathCache[\"/user/hand/left/input/aim/pose\"]},\n            {actionMap[\"r_aim_pose\"].action, pathCache[\"/user/hand/right/input/aim/pose\"]},\n\n            {actionMap[\"l_haptic\"].action, pathCache[\"/user/hand/left/output/haptic\"]},\n            {actionMap[\"r_haptic\"].action, pathCache[\"/user/hand/right/output/haptic\"]}\n        };\n\n        XrInteractionProfileSuggestedBinding suggestedBindings = {};\n        suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING;\n        suggestedBindings.next = NULL;\n        suggestedBindings.interactionProfile = interactionProfilePath;\n        suggestedBindings.suggestedBindings = bindings.data();\n        suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size();\n        result = xrSuggestInteractionProfileBindings(instance, &suggestedBindings);\n        if (!CheckResult(instance, result, \"xrSuggestInteractionProfileBindings (vive)\"))\n        {\n            return false;\n        }\n    }\n\n    // TODO\n#if 0\n    // microsoft mixed reality\n    {\n        XrPath interactionProfilePath = XR_NULL_PATH;\n        xrStringToPath(instance, \"/interaction_profiles/microsoft/motion_controller\", &interactionProfilePath);\n        std::vector<XrActionSuggestedBinding> bindings = {\n            {selectAction, squeezeClickPath[0]},\n            {selectAction, squeezeClickPath[1]},\n            {aimAction, aimPath[0]},\n            {aimAction, aimPath[1]},\n            {menuAction, menuClickPath[0]},\n            {menuAction, menuClickPath[1]},\n            {vibrateAction, hapticPath[0]},\n            {vibrateAction, hapticPath[1]}\n        };\n\n        XrInteractionProfileSuggestedBinding suggestedBindings = {};\n        suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING;\n        suggestedBindings.next = NULL;\n        suggestedBindings.interactionProfile = interactionProfilePath;\n        suggestedBindings.suggestedBindings = bindings.data();\n        suggestedBindings.countSuggestedBindings = (uint32_t)bindings.size();\n        result = xrSuggestInteractionProfileBindings(instance, &suggestedBindings);\n        if (!CheckResult(instance, result, \"xrSuggestInteractionProfileBindings\"))\n        {\n            return false;\n        }\n    }\n#endif\n\n    XrSessionActionSetsAttachInfo sasai = {};\n    sasai.type = XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO;\n    sasai.next = NULL;\n    sasai.countActionSets = 1;\n    sasai.actionSets = &actionSet;\n    result = xrAttachSessionActionSets(session, &sasai);\n    if (!CheckResult(instance, result, \"xrSessionActionSetsAttachInfo\"))\n    {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool CreateSpaces(XrInstance instance, XrSystemId systemId, XrSession session, XrSpace& stageSpace, XrSpace& viewSpace, XrSpaceLocation& viewSpaceLocation, XrSpaceVelocity& viewSpaceVelocity)\n{\n    XrResult result;\n    bool printReferenceSpaces = true;\n    if (printReferenceSpaces || printAll)\n    {\n        uint32_t referenceSpacesCount;\n        result = xrEnumerateReferenceSpaces(session, 0, &referenceSpacesCount, NULL);\n        if (!CheckResult(instance, result, \"xrEnumerateReferenceSpaces\"))\n        {\n            return false;\n        }\n\n        std::vector<XrReferenceSpaceType> referenceSpaces(referenceSpacesCount, XR_REFERENCE_SPACE_TYPE_VIEW);\n        result = xrEnumerateReferenceSpaces(session, referenceSpacesCount, &referenceSpacesCount, referenceSpaces.data());\n        if (!CheckResult(instance, result, \"xrEnumerateReferenceSpaces\"))\n        {\n            return false;\n        }\n\n        Log::D(\"referenceSpaces:\\n\");\n        for (uint32_t i = 0; i < referenceSpacesCount; i++)\n        {\n            switch (referenceSpaces[i])\n            {\n            case XR_REFERENCE_SPACE_TYPE_VIEW:\n                Log::D(\"    XR_REFERENCE_SPACE_TYPE_VIEW\\n\");\n                break;\n            case XR_REFERENCE_SPACE_TYPE_LOCAL:\n                Log::D(\"    XR_REFERENCE_SPACE_TYPE_LOCAL\\n\");\n                break;\n            case XR_REFERENCE_SPACE_TYPE_STAGE:\n                Log::D(\"    XR_REFERENCE_SPACE_TYPE_STAGE\\n\");\n                break;\n            default:\n                Log::D(\"    XR_REFERENCE_SPACE_TYPE_%d\\n\", referenceSpaces[i]);\n                break;\n            }\n        }\n    }\n\n    XrPosef identityPose;\n    identityPose.orientation = {0.0f, 0.0f, 0.0f, 1.0f};\n    identityPose.position = {0.0f, 0.0f, 0.0f};\n\n    XrReferenceSpaceCreateInfo rsci = {};\n    rsci.type = XR_TYPE_REFERENCE_SPACE_CREATE_INFO;\n    rsci.next = NULL;\n    rsci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE;\n    rsci.poseInReferenceSpace = identityPose;\n\n    result = xrCreateReferenceSpace(session, &rsci, &stageSpace);\n    if (!CheckResult(instance, result, \"xrCreateReferenceSpace\"))\n    {\n        return false;\n    }\n\n    rsci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW;\n    result = xrCreateReferenceSpace(session, &rsci, &viewSpace);\n    if (!CheckResult(instance, result, \"xrCreateReferenceSpace\"))\n    {\n        return false;\n    }\n\n    viewSpaceLocation.type = XR_TYPE_SPACE_LOCATION;\n    viewSpaceLocation.next = &viewSpaceVelocity;\n    viewSpaceVelocity.type = XR_TYPE_SPACE_VELOCITY;\n    viewSpaceVelocity.next = NULL;\n\n    return true;\n}\n\nstatic bool BeginSession(XrInstance instance, XrSystemId systemId, XrSession session)\n{\n    XrResult result;\n    XrViewConfigurationType stereoViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;\n    XrSessionBeginInfo sbi = {};\n    sbi.type = XR_TYPE_SESSION_BEGIN_INFO;\n    sbi.next = NULL;\n    sbi.primaryViewConfigurationType = stereoViewConfigType;\n\n    result = xrBeginSession(session, &sbi);\n    if (!CheckResult(instance, result, \"xrBeginSession\"))\n    {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool EndSession(XrInstance instance, XrSystemId systemId, XrSession session)\n{\n    XrResult result;\n    result = xrEndSession(session);\n    if (!CheckResult(instance, result, \"xrEndSession\"))\n    {\n        return false;\n    }\n\n    return true;\n}\n\nstatic bool CreateFrameBuffer(GLuint& frameBuffer)\n{\n    glGenFramebuffers(1, &frameBuffer);\n    return true;\n}\n\nstatic bool CreateSwapchains(XrInstance instance, XrSession session,\n                             const std::vector<XrViewConfigurationView>& viewConfigs,\n                             std::vector<XrBuddy::SwapchainInfo>& swapchains,\n                             std::vector<std::vector<XrBuddy::SwapchainImage>>& swapchainImages)\n{\n    XrResult result;\n    uint32_t swapchainFormatCount;\n    result = xrEnumerateSwapchainFormats(session, 0, &swapchainFormatCount, NULL);\n    if (!CheckResult(instance, result, \"xrEnumerateSwapchainFormats\"))\n    {\n        return false;\n    }\n\n    std::vector<int64_t> swapchainFormats(swapchainFormatCount);\n    result = xrEnumerateSwapchainFormats(session, swapchainFormatCount, &swapchainFormatCount, swapchainFormats.data());\n    if (!CheckResult(instance, result, \"xrEnumerateSwapchainFormats\"))\n    {\n        return false;\n    }\n\n    if (printAll)\n    {\n        Log::D(\"xrEnumerateSwapchainFormats, count = %d\\n\", swapchainFormatCount);\n        for (uint32_t i = 0; i < swapchainFormatCount; i++)\n        {\n            Log::D(\"    format[%d] = 0x%x\\n\", i, swapchainFormats[i]);\n        }\n    }\n\n    std::vector<uint64_t> formats = {GL_R11F_G11F_B10F_EXT, GL_RGB16F_EXT, GL_RGBA};\n    uint32_t foundFormatIndex = swapchainFormatCount;\n    for (auto&& desiredFormat : formats)\n    {\n        foundFormatIndex = swapchainFormatCount;\n        for (uint32_t i = 0; i < swapchainFormatCount; i++)\n        {\n            if (swapchainFormats[i] == desiredFormat)\n            {\n                if (printAll)\n                {\n                    Log::D(\"found desired framebuffer format 0x%x!\\n\", desiredFormat);\n                }\n                foundFormatIndex = i;\n                break;\n            }\n        }\n        if (foundFormatIndex != swapchainFormatCount)\n        {\n            break;\n        }\n    }\n\n    int64_t format;\n    if (foundFormatIndex == swapchainFormatCount)\n    {\n        Log::W(\"could not find any desired swapchain format!\\n\");\n        format = swapchainFormats[0];\n    }\n    else\n    {\n        format = swapchainFormats[foundFormatIndex];\n    }\n\n    std::vector<uint32_t> swapchainLengths(viewConfigs.size());\n\n    swapchains.resize(viewConfigs.size());\n\n    for (uint32_t i = 0; i < viewConfigs.size(); i++)\n    {\n        XrSwapchainCreateInfo sci = {};\n        sci.type = XR_TYPE_SWAPCHAIN_CREATE_INFO;\n        sci.next = NULL;\n        sci.createFlags = 0;\n        sci.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;\n        sci.format = format;\n        sci.sampleCount = 1;\n        sci.width = viewConfigs[i].recommendedImageRectWidth;\n        sci.height = viewConfigs[i].recommendedImageRectHeight;\n        sci.faceCount = 1;\n        sci.arraySize = 1;\n        sci.mipCount = 1;\n\n        XrSwapchain swapchainHandle = XR_NULL_HANDLE;\n        result = xrCreateSwapchain(session, &sci, &swapchainHandle);\n        if (!CheckResult(instance, result, \"xrCreateSwapchain\"))\n        {\n            return false;\n        }\n\n        swapchains[i].handle = swapchainHandle;\n        swapchains[i].width = sci.width;\n        swapchains[i].height = sci.height;\n\n        uint32_t length;\n        result = xrEnumerateSwapchainImages(swapchains[i].handle, 0, &length, nullptr);\n        if (!CheckResult(instance, result, \"xrEnumerateSwapchainImages\"))\n        {\n            return false;\n        }\n        swapchainLengths[i] = length;\n    }\n\n    swapchainImages.resize(viewConfigs.size());\n    for (uint32_t i = 0; i < viewConfigs.size(); i++)\n    {\n        swapchainImages[i].resize(swapchainLengths[i]);\n        for (uint32_t j = 0; j < swapchainLengths[i]; j++)\n        {\n#if defined(XR_USE_GRAPHICS_API_OPENGL)\n            swapchainImages[i][j].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR;\n#elif defined(XR_USE_GRAPHICS_API_OPENGL_ES)\n            swapchainImages[i][j].type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR;\n#endif\n            swapchainImages[i][j].next = NULL;\n        }\n\n        result = xrEnumerateSwapchainImages(swapchains[i].handle, swapchainLengths[i], &swapchainLengths[i],\n                                            (XrSwapchainImageBaseHeader*)(swapchainImages[i].data()));\n        if (!CheckResult(instance, result, \"xrEnumerateSwapchainImages\"))\n        {\n            return false;\n        }\n    }\n\n    return true;\n}\n\nstatic GLuint CreateDepthTexture(GLuint colorTexture, GLint width, GLint height)\n{\n    glBindTexture(GL_TEXTURE_2D, colorTexture);\n\n    uint32_t depthTexture;\n    glGenTextures(1, &depthTexture);\n    glBindTexture(GL_TEXTURE_2D, depthTexture);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);\n    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);\n    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);\n    return depthTexture;\n}\n\nXrBuddy::XrBuddy(MainContext& mainContextIn, const glm::vec2& nearFarIn):\n    mainContext(mainContextIn)\n{\n    nearFar = nearFarIn;\n\n#ifdef XR_USE_GRAPHICS_API_OPENGL\n    std::vector<const char*> requiredExtensionVec = {XR_KHR_OPENGL_ENABLE_EXTENSION_NAME};\n#elif defined(XR_USE_GRAPHICS_API_OPENGL_ES)\n    std::vector<const char*> requiredExtensionVec = {XR_KHR_OPENGL_ES_ENABLE_EXTENSION_NAME};\n#endif\n    std::vector<const char*> optionalExtensionVec = {XR_FB_COLOR_SPACE_EXTENSION_NAME};\n    std::vector<const char*> extensionVec;\n\n#ifdef __ANDROID__\n    PFN_xrInitializeLoaderKHR xrInitializeLoaderKHR;\n    xrGetInstanceProcAddr(XR_NULL_HANDLE, \"xrInitializeLoaderKHR\", (PFN_xrVoidFunction*)&xrInitializeLoaderKHR);\n    if (xrInitializeLoaderKHR != NULL)\n    {\n        XrLoaderInitInfoAndroidKHR loaderInitializeInfoAndroid = {};\n        loaderInitializeInfoAndroid.type = XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR;\n        loaderInitializeInfoAndroid.applicationVM = mainContext.androidApp->activity->vm;\n        loaderInitializeInfoAndroid.applicationContext = mainContext.androidApp->activity->clazz;\n        xrInitializeLoaderKHR((XrLoaderInitInfoBaseHeaderKHR*)&loaderInitializeInfoAndroid);\n    }\n#endif\n\n    if (!EnumerateExtensions(extensionProps))\n    {\n        return;\n    }\n\n    for (auto&& ext : requiredExtensionVec)\n    {\n        if (!ExtensionSupported(extensionProps, ext))\n        {\n            Log::W(\"required extension \\\"%s\\\" not supported!\\n\", ext);\n            return;\n        }\n        extensionVec.push_back(ext);\n    }\n\n    for (auto&& ext : optionalExtensionVec)\n    {\n        if (ExtensionSupported(extensionProps, ext))\n        {\n            extensionVec.push_back(ext);\n        }\n    }\n\n    if (!EnumerateApiLayers(layerProps))\n    {\n        return;\n    }\n\n    if (!CreateInstance(instance, extensionVec))\n    {\n        return;\n    }\n\n    if (!GetSystemId(instance, systemId))\n    {\n        return;\n    }\n\n    if (!GetSystemProperties(instance, systemId, systemProperties))\n    {\n        return;\n    }\n\n    if (!SupportsVR(instance, systemId))\n    {\n        Log::E(\"System doesn't support VR\\n\");\n        return;\n    }\n\n    if (!EnumerateViewConfigs(instance, systemId, viewConfigs))\n    {\n        return;\n    }\n\n    constructorSucceded = true;\n}\n\nbool XrBuddy::Init()\n{\n    if (!constructorSucceded)\n    {\n        return false;\n    }\n\n    if (!CreateSession(instance, systemId, session, mainContext))\n    {\n        return false;\n    }\n\n    if (ExtensionSupported(extensionProps, XR_FB_COLOR_SPACE_EXTENSION_NAME))\n    {\n        SetColorSpace(instance, session, XR_COLOR_SPACE_REC709_FB);\n    }\n\n    if (!CreateActions(instance, systemId, session, actionSet, actionMap))\n    {\n        return false;\n    }\n\n    if (!CreateSpaces(instance, systemId, session, stageSpace, viewSpace, viewSpaceLocation, viewSpaceVelocity))\n    {\n        return false;\n    }\n\n    if (!CreateFrameBuffer(frameBuffer))\n    {\n        return false;\n    }\n\n    if (!CreateSwapchains(instance, session, viewConfigs, swapchains, swapchainImages))\n    {\n        return false;\n    }\n\n    return true;\n}\n\nbool XrBuddy::PollEvents()\n{\n    ZoneScoped;\n\n    XrEventDataBuffer xrEvent = {};\n    xrEvent.type = XR_TYPE_EVENT_DATA_BUFFER;\n    xrEvent.next = NULL;\n\n    while (xrPollEvent(instance, &xrEvent) == XR_SUCCESS)\n    {\n        switch (xrEvent.type)\n        {\n        case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING:\n            // Receiving the XrEventDataInstanceLossPending event structure indicates that the application is about to lose the indicated XrInstance at the indicated lossTime in the future.\n            // The application should call xrDestroyInstance and relinquish any instance-specific resources.\n            // This typically occurs to make way for a replacement of the underlying runtime, such as via a software update.\n            Log::D(\"xrEvent: XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING\\n\");\n            break;\n        case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED:\n        {\n            // Receiving the XrEventDataSessionStateChanged event structure indicates that the application has changed lifecycle stat.e\n            Log::D(\"xrEvent: XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED -> \");\n            XrEventDataSessionStateChanged* ssc = (XrEventDataSessionStateChanged*)&xrEvent;\n            state = ssc->state;\n            switch (state)\n            {\n            case XR_SESSION_STATE_IDLE:\n                // The initial state after calling xrCreateSession or returned to after calling xrEndSession.\n                Log::D(\"XR_SESSION_STATE_IDLE\\n\");\n                break;\n            case XR_SESSION_STATE_READY:\n                // The application is ready to call xrBeginSession and sync its frame loop with the runtime.\n                Log::D(\"XR_SESSION_STATE_READY\\n\");\n                if (!BeginSession(instance, systemId, session))\n                {\n                    return false;\n                }\n                sessionReady = true;\n                break;\n            case XR_SESSION_STATE_SYNCHRONIZED:\n                // The application has synced its frame loop with the runtime but is not visible to the user.\n                Log::D(\"XR_SESSION_STATE_SYNCHRONIZED\\n\");\n                break;\n            case XR_SESSION_STATE_VISIBLE:\n                // The application has synced its frame loop with the runtime and is visible to the user but cannot receive XR input.\n                Log::D(\"XR_SESSION_STATE_VISIBLE\\n\");\n                break;\n            case XR_SESSION_STATE_FOCUSED:\n                // The application has synced its frame loop with the runtime, is visible to the user and can receive XR input.\n                Log::D(\"XR_SESSION_STATE_FOCUSED\\n\");\n                break;\n            case XR_SESSION_STATE_STOPPING:\n                Log::D(\"XR_SESSION_STATE_STOPPING\\n\");\n                // The application should exit its frame loop and call xrEndSession.\n                if (!EndSession(instance, systemId, session))\n                {\n                    return false;\n                }\n                sessionReady = false;\n                break;\n            case XR_SESSION_STATE_LOSS_PENDING:\n                Log::D(\"XR_SESSION_STATE_LOSS_PENDING\\n\");\n                // The session is in the process of being lost. The application should destroy the current session and can optionally recreate it.\n                break;\n            case XR_SESSION_STATE_EXITING:\n                Log::D(\"XR_SESSION_STATE_EXITING\\n\");\n                // The application should end its XR experience and not automatically restart it.\n                break;\n            default:\n                Log::D(\"XR_SESSION_STATE_??? %d\\n\", (int)state);\n                break;\n            }\n            break;\n        }\n        case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING:\n            // The XrEventDataReferenceSpaceChangePending event is sent to the application to notify it that the origin (and perhaps the bounds) of a reference space is changing.\n            Log::D(\"XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING\\n\");\n            break;\n        case XR_TYPE_EVENT_DATA_EVENTS_LOST:\n            // Receiving the XrEventDataEventsLost event structure indicates that the event queue overflowed and some events were removed at the position within the queue at which this event was found.\n            Log::D(\"xrEvent: XR_TYPE_EVENT_DATA_EVENTS_LOST\\n\");\n            break;\n        case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED:\n            // The XrEventDataInteractionProfileChanged event is sent to the application to notify it that the active input form factor for one or more top level user paths has changed.:\n            Log::D(\"XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED\\n\");\n            break;\n        default:\n            Log::D(\"Unhandled event type %d\\n\", xrEvent.type);\n            break;\n        }\n\n        xrEvent.type = XR_TYPE_EVENT_DATA_BUFFER;\n        xrEvent.next = NULL;\n    }\n\n    return true;\n}\n\nbool XrBuddy::SyncInput()\n{\n    ZoneScoped;\n\n    if (state == XR_SESSION_STATE_FOCUSED)\n    {\n        XrResult result;\n\n        XrActiveActionSet aas = {};\n        aas.actionSet = actionSet;\n        aas.subactionPath = XR_NULL_PATH;\n        XrActionsSyncInfo asi = {};\n        asi.type = XR_TYPE_ACTIONS_SYNC_INFO;\n        asi.next = NULL;\n        asi.countActiveActionSets = 1;\n        asi.activeActionSets = &aas;\n        result = xrSyncActions(session, &asi);\n        if (!CheckResult(instance, result, \"xrSyncActions\"))\n        {\n            return false;\n        }\n\n        bool printActions = false;\n\n        for (auto& iter : actionMap)\n        {\n            ActionInfo& actionInfo = iter.second;\n            XrActionStateGetInfo getInfo = {};\n            getInfo.type = XR_TYPE_ACTION_STATE_GET_INFO;\n            getInfo.next = NULL;\n            getInfo.action = actionInfo.action;\n            getInfo.subactionPath = 0;\n            iter.second.u.boolState.next = NULL;\n            switch (iter.second.type)\n            {\n            case XR_ACTION_TYPE_BOOLEAN_INPUT:\n                iter.second.u.boolState.type = XR_TYPE_ACTION_STATE_BOOLEAN;\n                result = xrGetActionStateBoolean(session, &getInfo, &iter.second.u.boolState);\n                if (!CheckResult(instance, result, \"xrGetActionStateBoolean\"))\n                {\n                    return false;\n                }\n                if (printActions && iter.second.u.boolState.changedSinceLastSync)\n                {\n                    Log::D(\"action %s:\\n\", iter.first.c_str());\n                    Log::D(\"    currentState: %s\\n\", iter.second.u.boolState.currentState ? \"true\" : \"false\");\n                    Log::D(\"    changedSinceLastSync: %s\\n\", iter.second.u.boolState.changedSinceLastSync ? \"true\" : \"false\");\n                    Log::D(\"    lastChangeTime: %lld\\n\", iter.second.u.boolState.lastChangeTime);\n                    Log::D(\"    isActive: %s\\n\", iter.second.u.boolState.isActive ? \"true\" : \"false\");\n                }\n                break;\n            case XR_ACTION_TYPE_FLOAT_INPUT:\n                iter.second.u.floatState.type = XR_TYPE_ACTION_STATE_FLOAT;\n                result = xrGetActionStateFloat(session, &getInfo, &iter.second.u.floatState);\n                if (!CheckResult(instance, result, \"xrGetActionStateFloat\"))\n                {\n                    return false;\n                }\n                if (printActions && iter.second.u.floatState.changedSinceLastSync)\n                {\n                    Log::D(\"action %s:\\n\", iter.first.c_str());\n                    Log::D(\"    currentState: %.5f\\n\", iter.second.u.floatState.currentState);\n                    Log::D(\"    changedSinceLastSync: %s\\n\", iter.second.u.floatState.changedSinceLastSync ? \"true\" : \"false\");\n                    Log::D(\"    lastChangeTime: %lld\\n\", iter.second.u.floatState.lastChangeTime);\n                    Log::D(\"    isActive: %s\\n\", iter.second.u.floatState.isActive ? \"true\" : \"false\");\n                }\n                break;\n            case XR_ACTION_TYPE_VECTOR2F_INPUT:\n                iter.second.u.vec2State.type = XR_TYPE_ACTION_STATE_VECTOR2F;\n                result = xrGetActionStateVector2f(session, &getInfo, &iter.second.u.vec2State);\n                if (!CheckResult(instance, result, \"xrGetActionStateVector2f\"))\n                {\n                    return false;\n                }\n                if (printActions && iter.second.u.vec2State.changedSinceLastSync)\n                {\n                    Log::D(\"action %s:\\n\", iter.first.c_str());\n                    Log::D(\"    currentState: (%.5f, %.5f)\\n\", iter.second.u.vec2State.currentState.x, iter.second.u.vec2State.currentState.y);\n                    Log::D(\"    changedSinceLastSync: %s\\n\", iter.second.u.vec2State.changedSinceLastSync ? \"true\" : \"false\");\n                    Log::D(\"    lastChangeTime: %lld\\n\", iter.second.u.vec2State.lastChangeTime);\n                    Log::D(\"    isActive: %s\\n\", iter.second.u.vec2State.isActive ? \"true\" : \"false\");\n                }\n                break;\n            case XR_ACTION_TYPE_POSE_INPUT:\n                iter.second.u.poseState.type = XR_TYPE_ACTION_STATE_POSE;\n                result = xrGetActionStatePose(session, &getInfo, &iter.second.u.poseState);\n                if (!CheckResult(instance, result, \"xrGetActionStatePose\"))\n                {\n                    return false;\n                }\n                break;\n            default:\n                break;\n            }\n        }\n    }\n\n    return true;\n}\n\nbool XrBuddy::GetActionBool(const std::string& actionName, bool* value, bool* valid, bool* changed) const\n{\n    auto iter = actionMap.find(actionName);\n    if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_BOOLEAN_INPUT)\n    {\n        return false;\n    }\n\n    if (state != XR_SESSION_STATE_FOCUSED)\n    {\n        *value = false;\n        *valid = false;\n        *changed = false;\n        return true;\n    }\n\n    *value = iter->second.u.boolState.currentState;\n    *valid = iter->second.u.boolState.isActive;\n    *changed = iter->second.u.boolState.changedSinceLastSync;\n    return true;\n}\n\nbool XrBuddy::GetActionFloat(const std::string& actionName, float* value, bool* valid, bool* changed) const\n{\n    auto iter = actionMap.find(actionName);\n    if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_FLOAT_INPUT)\n    {\n        return false;\n    }\n\n    if (state != XR_SESSION_STATE_FOCUSED)\n    {\n        *value = 0.0f;\n        *valid = false;\n        *changed = false;\n        return true;\n    }\n\n    *value = iter->second.u.floatState.currentState;\n    *valid = iter->second.u.floatState.isActive;\n    *changed = iter->second.u.floatState.changedSinceLastSync;\n    return true;\n}\n\nbool XrBuddy::GetActionVec2(const std::string& actionName, glm::vec2* value, bool* valid, bool* changed) const\n{\n    auto iter = actionMap.find(actionName);\n    if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_VECTOR2F_INPUT)\n    {\n        return false;\n    }\n\n    if (state != XR_SESSION_STATE_FOCUSED)\n    {\n        *value = glm::vec2(0.0f);\n        *valid = false;\n        *changed = false;\n        return true;\n    }\n\n    *value = glm::vec2(iter->second.u.vec2State.currentState.x, iter->second.u.vec2State.currentState.y);\n    *valid = iter->second.u.vec2State.isActive;\n    *changed = iter->second.u.vec2State.changedSinceLastSync;\n    return true;\n}\n\nbool XrBuddy::GetActionPosition(const std::string& actionName, glm::vec3* value, bool* valid, bool* tracked) const\n{\n    // special case for \"head_pose\"\n    if (actionName == \"head_pose\")\n    {\n        *value = glm::vec3(viewSpaceLocation.pose.position.x, viewSpaceLocation.pose.position.y, viewSpaceLocation.pose.position.z);\n        *valid = viewSpaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT;\n        *tracked = viewSpaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT;\n        return true;\n    }\n\n    auto iter = actionMap.find(actionName);\n    if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_POSE_INPUT)\n    {\n        return false;\n    }\n\n    if (state != XR_SESSION_STATE_FOCUSED || !iter->second.u.poseState.isActive)\n    {\n        *value = glm::vec3(0.0f);\n        *valid = false;\n        *tracked = false;\n        return true;\n    }\n\n    *value = glm::vec3(iter->second.spaceLocation.pose.position.x, iter->second.spaceLocation.pose.position.y, iter->second.spaceLocation.pose.position.z);\n    *valid = iter->second.spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT;\n    *tracked = iter->second.spaceLocation.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT;\n    return true;\n}\n\nbool XrBuddy::GetActionOrientation(const std::string& actionName, glm::quat* value, bool* valid, bool* tracked) const\n{\n    // special case for \"head_pose\"\n    if (actionName == \"head_pose\")\n    {\n        *value = glm::quat(viewSpaceLocation.pose.orientation.w, viewSpaceLocation.pose.orientation.x, viewSpaceLocation.pose.orientation.y, viewSpaceLocation.pose.orientation.z);\n        *valid = viewSpaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;\n        *tracked = viewSpaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT;\n        return true;\n    }\n\n    auto iter = actionMap.find(actionName);\n    if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_POSE_INPUT)\n    {\n        return false;\n    }\n\n    if (state != XR_SESSION_STATE_FOCUSED || !iter->second.u.poseState.isActive)\n    {\n        *value = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);\n        *valid = false;\n        *tracked = false;\n        return true;\n    }\n\n    *value = glm::quat(iter->second.spaceLocation.pose.orientation.w, iter->second.spaceLocation.pose.orientation.x, iter->second.spaceLocation.pose.orientation.y, iter->second.spaceLocation.pose.orientation.z);\n    *valid = iter->second.spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;\n    *tracked = iter->second.spaceLocation.locationFlags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT;\n    return true;\n}\n\nbool XrBuddy::GetActionLinearVelocity(const std::string& actionName, glm::vec3* value, bool* valid) const\n{\n    // special case for \"head_pose\"\n    if (actionName == \"head_pose\")\n    {\n        *value = glm::vec3(viewSpaceVelocity.linearVelocity.x, viewSpaceVelocity.linearVelocity.y, viewSpaceVelocity.linearVelocity.z);\n        *valid = viewSpaceVelocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT;\n        return true;\n    }\n\n    auto iter = actionMap.find(actionName);\n    if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_POSE_INPUT)\n    {\n        return false;\n    }\n\n    if (state != XR_SESSION_STATE_FOCUSED || !iter->second.u.poseState.isActive)\n    {\n        *value = glm::vec3(0.0f);\n        *valid = false;\n        return true;\n    }\n\n    *value = glm::vec3(iter->second.spaceVelocity.linearVelocity.x, iter->second.spaceVelocity.linearVelocity.y, iter->second.spaceVelocity.linearVelocity.z);\n    *valid = iter->second.spaceVelocity.velocityFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT;\n    return true;\n}\n\nbool XrBuddy::GetActionAngularVelocity(const std::string& actionName, glm::vec3* value, bool* valid) const\n{\n\n    // special case for \"head_pose\"\n    if (actionName == \"head_pose\")\n    {\n        *value = glm::vec3(viewSpaceVelocity.angularVelocity.x, viewSpaceVelocity.angularVelocity.y, viewSpaceVelocity.angularVelocity.z);\n        *valid = viewSpaceVelocity.velocityFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT;\n        return true;\n    }\n\n    auto iter = actionMap.find(actionName);\n    if (iter == actionMap.end() || iter->second.type != XR_ACTION_TYPE_POSE_INPUT)\n    {\n        return false;\n    }\n\n    if (state != XR_SESSION_STATE_FOCUSED || !iter->second.u.poseState.isActive)\n    {\n        *value = glm::vec3(0.0f);\n        *valid = false;\n        return true;\n    }\n\n    *value = glm::vec3(iter->second.spaceVelocity.angularVelocity.x, iter->second.spaceVelocity.angularVelocity.y, iter->second.spaceVelocity.angularVelocity.z);\n    *valid = iter->second.spaceVelocity.velocityFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT;\n    return true;\n}\n\nuint32_t XrBuddy::GetColorTexture() const\n{\n    return prevLastColorTexture;\n}\n\nvoid XrBuddy::CycleColorSpace()\n{\n    static int i = 0;\n    Log::D(\"SETTING COLOR SPACE -> %d\\n\", i);\n    SetColorSpace(instance, session, (XrColorSpaceFB)i);\n    i = (i + 1) % (XR_COLOR_SPACE_ADOBE_RGB_FB + 1);\n}\n\nbool XrBuddy::LocateSpaces(XrTime predictedDisplayTime)\n{\n    ZoneScoped;\n\n    viewSpaceLocation = {};\n    viewSpaceVelocity = {};\n    viewSpaceLocation.type = XR_TYPE_SPACE_LOCATION;\n    viewSpaceLocation.next = &viewSpaceVelocity;\n    viewSpaceVelocity.type = XR_TYPE_SPACE_VELOCITY;\n    viewSpaceVelocity.next = NULL;\n\n    XrResult result;\n    if (state == XR_SESSION_STATE_FOCUSED)\n    {\n        for (auto& iter : actionMap)\n        {\n            if (iter.second.type == XR_ACTION_TYPE_POSE_INPUT && iter.second.u.poseState.isActive)\n            {\n                // link location -> velocity\n                iter.second.spaceLocation.next = &iter.second.spaceVelocity;\n                result = xrLocateSpace(iter.second.space, stageSpace, predictedDisplayTime, &iter.second.spaceLocation);\n                if (!CheckResult(instance, result, \"xrLocateSpace\"))\n                {\n                    return false;\n                }\n            }\n        }\n\n        result = xrLocateSpace(viewSpace, stageSpace, predictedDisplayTime, &viewSpaceLocation);\n        if (!CheckResult(instance, result, \"xrLocateSpace\"))\n        {\n            return false;\n        }\n    }\n    return true;\n}\n\nbool XrBuddy::SessionReady() const\n{\n    return sessionReady;\n}\n\nbool XrBuddy::RenderFrame()\n{\n    ZoneScoped;\n\n    if (state == XR_SESSION_STATE_READY ||\n        state == XR_SESSION_STATE_SYNCHRONIZED ||\n        state == XR_SESSION_STATE_VISIBLE ||\n        state == XR_SESSION_STATE_FOCUSED)\n    {\n        XrFrameState fs = {};\n        fs.type = XR_TYPE_FRAME_STATE;\n        fs.next = NULL;\n\n        XrFrameWaitInfo fwi = {};\n        fwi.type = XR_TYPE_FRAME_WAIT_INFO;\n        fwi.next = NULL;\n\n        XrResult result;\n        {\n            ZoneScopedNC(\"xrWaitFrame\", tracy::Color::Red4);\n            result = xrWaitFrame(session, &fwi, &fs);\n            if (!CheckResult(instance, result, \"xrWaitFrame\"))\n            {\n                return false;\n            }\n        }\n        lastPredictedDisplayTime = fs.predictedDisplayTime;\n\n        XrFrameBeginInfo fbi = {};\n        fbi.type = XR_TYPE_FRAME_BEGIN_INFO;\n        fbi.next = NULL;\n        {\n            ZoneScopedNC(\"xrBeginFrame\", tracy::Color::DarkGreen);\n            result = xrBeginFrame(session, &fbi);\n            if (!CheckResult(instance, result, \"xrBeginFrame\"))\n            {\n                return false;\n            }\n        }\n\n        std::vector<XrCompositionLayerBaseHeader*> layers;\n        XrCompositionLayerProjection layer = {};\n        layer.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION;\n        layer.next = NULL;\n\n        std::vector<XrCompositionLayerProjectionView> projectionLayerViews;\n        if (fs.shouldRender == XR_TRUE)\n        {\n            if (!LocateSpaces(fs.predictedDisplayTime))\n            {\n                return false;\n            }\n            if (RenderLayer(fs.predictedDisplayTime, projectionLayerViews, layer))\n            {\n                layers.push_back(reinterpret_cast<XrCompositionLayerBaseHeader*>(&layer));\n            }\n        }\n\n        XrFrameEndInfo fei = {};\n        fei.type = XR_TYPE_FRAME_END_INFO;\n        fei.next = NULL;\n        fei.displayTime = fs.predictedDisplayTime;\n        fei.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;\n        fei.layerCount = (uint32_t)layers.size();\n        fei.layers = layers.data();\n\n        {\n            ZoneScopedNC(\"xrEndFrame\", tracy::Color::Red4);\n\n            result = xrEndFrame(session, &fei);\n            if (!CheckResult(instance, result, \"xrEndFrame\"))\n            {\n                return false;\n            }\n        }\n    }\n\n    return true;\n}\n\nbool XrBuddy::Shutdown()\n{\n    for (auto& swapchain : swapchains)\n    {\n        xrDestroySwapchain(swapchain.handle);\n    }\n    swapchains.clear();\n    swapchainImages.clear();\n\n    for (auto& iter : colorToDepthMap)\n    {\n        glDeleteTextures(1, &iter.second);\n    }\n    colorToDepthMap.clear();\n\n    if (frameBuffer)\n    {\n        glDeleteFramebuffers(1, &frameBuffer);\n        frameBuffer = 0;\n    }\n\n    if (stageSpace != XR_NULL_HANDLE)\n    {\n        xrDestroySpace(stageSpace);\n        stageSpace = XR_NULL_HANDLE;\n    }\n\n    if (viewSpace != XR_NULL_HANDLE)\n    {\n        xrDestroySpace(viewSpace);\n        viewSpace = XR_NULL_HANDLE;\n    }\n\n    for (auto iter : actionMap)\n    {\n        if (iter.second.space != XR_NULL_HANDLE)\n        {\n            xrDestroySpace(iter.second.space);\n        }\n        if (iter.second.action != XR_NULL_HANDLE)\n        {\n            xrDestroyAction(iter.second.action);\n        }\n    }\n    actionMap.clear();\n\n\n    if (actionSet != XR_NULL_HANDLE)\n    {\n        xrDestroyActionSet(actionSet);\n        actionSet = XR_NULL_HANDLE;\n    }\n\n    if (session != XR_NULL_HANDLE)\n    {\n        xrDestroySession(session);\n        session = XR_NULL_HANDLE;\n    }\n\n    // on oculus runtime destroying the instance causes a crash on shutdown... so don't...\n    /*\n    if (instance != XR_NULL_HANDLE)\n    {\n        xrDestroyInstance(instance);\n        instance = XR_NULL_HANDLE;\n    }\n    */\n\n    return true;\n}\n\nbool XrBuddy::RenderLayer(XrTime predictedDisplayTime,\n                          std::vector<XrCompositionLayerProjectionView>& projectionLayerViews,\n                          XrCompositionLayerProjection& layer)\n{\n    ZoneScoped;\n\n    XrViewState viewState = {};\n    viewState.type = XR_TYPE_VIEW_STATE;\n    viewState.next = NULL;\n\n    uint32_t viewCapacityInput = (uint32_t)viewConfigs.size();\n    uint32_t viewCountOutput;\n\n    std::vector<XrView> views(viewConfigs.size());\n    for (size_t i = 0; i < viewConfigs.size(); i++)\n    {\n        views[i] = {};\n        views[i].type = XR_TYPE_VIEW;\n        views[i].next = NULL;\n    }\n\n    XrViewLocateInfo vli = {};\n    vli.type = XR_TYPE_VIEW_LOCATE_INFO;\n    vli.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;\n    vli.displayTime = predictedDisplayTime;\n    vli.space = stageSpace;\n    vli.next = NULL;\n    XrResult result = xrLocateViews(session, &vli, &viewState, viewCapacityInput, &viewCountOutput, views.data());\n    if (!CheckResult(instance, result, \"xrLocateViews\"))\n    {\n        return false;\n    }\n\n    if (XR_UNQUALIFIED_SUCCESS(result))\n    {\n        assert(viewCountOutput == viewCapacityInput);\n        assert(viewCountOutput == viewConfigs.size());\n        assert(viewCountOutput == swapchains.size());\n\n        projectionLayerViews.resize(viewCountOutput);\n\n        // Render view to the appropriate part of the swapchain image.\n        for (uint32_t i = 0; i < viewCountOutput; i++)\n        {\n            // Each view has a separate swapchain which is acquired, rendered to, and released.\n            const SwapchainInfo& viewSwapchain = swapchains[i];\n\n            XrSwapchainImageAcquireInfo ai = {};\n            ai.type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO;\n            ai.next = NULL;\n\n            uint32_t swapchainImageIndex;\n            result = xrAcquireSwapchainImage(viewSwapchain.handle, &ai, &swapchainImageIndex);\n            if (!CheckResult(instance, result, \"xrAcquireSwapchainImage\"))\n            {\n                return false;\n            }\n\n            XrSwapchainImageWaitInfo wi = {};\n            wi.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO;\n            wi.next = NULL;\n            wi.timeout = XR_INFINITE_DURATION;\n            result = xrWaitSwapchainImage(viewSwapchain.handle, &wi);\n            if (!CheckResult(instance, result, \"xrWaitSwapchainImage\"))\n            {\n                return false;\n            }\n\n            projectionLayerViews[i] = {};\n            projectionLayerViews[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;\n            projectionLayerViews[i].pose = views[i].pose;\n            projectionLayerViews[i].fov = views[i].fov;\n            projectionLayerViews[i].subImage.swapchain = viewSwapchain.handle;\n            projectionLayerViews[i].subImage.imageRect.offset = {0, 0};\n            projectionLayerViews[i].subImage.imageRect.extent = {viewSwapchain.width, viewSwapchain.height};\n\n            const SwapchainImage& swapchainImage = swapchainImages[i][swapchainImageIndex];\n\n            // find or create the depthTexture associated with this colorTexture\n            const uint32_t colorTexture = swapchainImage.image;\n            if (i == 0)\n            {\n                prevLastColorTexture = lastColorTexture;\n                lastColorTexture = colorTexture;  // save for rendering onto desktop.\n            }\n\n            auto iter = colorToDepthMap.find(colorTexture);\n            if (iter == colorToDepthMap.end())\n            {\n                const uint32_t depthTexture = CreateDepthTexture(colorTexture, viewSwapchain.width, viewSwapchain.height);\n                iter = colorToDepthMap.insert(std::make_pair(colorTexture, depthTexture)).first;\n            }\n\n            RenderView(projectionLayerViews[i], frameBuffer, iter->first, iter->second, i);\n\n            XrSwapchainImageReleaseInfo ri = {};\n            ri.type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO;\n            ri.next = NULL;\n            result = xrReleaseSwapchainImage(viewSwapchain.handle, &ri);\n            if (!CheckResult(instance, result, \"xrReleaseSwapchainImage\"))\n            {\n                return false;\n            }\n\n            layer.space = stageSpace;\n            layer.viewCount = (uint32_t)projectionLayerViews.size();\n            layer.views = projectionLayerViews.data();\n        }\n    }\n\n    return true;\n}\n\nvoid XrBuddy::RenderView(const XrCompositionLayerProjectionView& layerView, uint32_t frameBuffer,\n                         uint32_t colorTexture, uint32_t depthTexture, int32_t viewNum)\n{\n    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);\n\n    glViewport(static_cast<GLint>(layerView.subImage.imageRect.offset.x),\n               static_cast<GLint>(layerView.subImage.imageRect.offset.y),\n               static_cast<GLsizei>(layerView.subImage.imageRect.extent.width),\n               static_cast<GLsizei>(layerView.subImage.imageRect.extent.height));\n\n    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0);\n    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTexture, 0);\n\n    const float tanLeft = tanf(layerView.fov.angleLeft);\n    const float tanRight = tanf(layerView.fov.angleRight);\n    const float tanDown = tanf(layerView.fov.angleDown);\n    const float tanUp = tanf(layerView.fov.angleUp);\n\n    glm::mat4 projMat;\n    CreateProjection(glm::value_ptr(projMat), GRAPHICS_OPENGL, tanLeft, tanRight, tanUp, tanDown, nearFar.x, nearFar.y);\n\n    const auto& pose = layerView.pose;\n    glm::quat eyeRot(pose.orientation.w, pose.orientation.x, pose.orientation.y, pose.orientation.z);\n    glm::vec3 eyePos(pose.position.x, pose.position.y, pose.position.z);\n    glm::mat4 eyeMat = MakeMat4(eyeRot, eyePos);\n    glm::vec4 viewport(layerView.subImage.imageRect.offset.x, layerView.subImage.imageRect.offset.y,\n                       layerView.subImage.imageRect.extent.width, layerView.subImage.imageRect.extent.height);\n    renderCallback(projMat, eyeMat, viewport, nearFar, viewNum);\n\n    glBindFramebuffer(GL_FRAMEBUFFER, 0);\n}\n"
  },
  {
    "path": "src/core/xrbuddy.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <array>\n#include <functional>\n#include <map>\n#include <string>\n#include <vector>\n\n#if defined(WIN32)\n#define XR_USE_PLATFORM_WIN32 1\n#define XR_USE_GRAPHICS_API_OPENGL 1\n#include <Windows.h>\n#elif defined(__ANDROID__)\n#define XR_USE_PLATFORM_ANDROID 1\n#define XR_USE_GRAPHICS_API_OPENGL_ES 1\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#include <jni.h>\n#include <android_native_app_glue.h>\n#elif defined(__linux__)\n#define XR_USE_PLATFORM_XLIB 1\n#define XR_USE_GRAPHICS_API_OPENGL 1\n#include <X11/Xlib.h>\n#include <X11/Xatom.h>\n#include <X11/extensions/xf86vmode.h>  // for fullscreen video mode\n#include <X11/extensions/Xrandr.h>     // for resolution changes\n#include <GL/glew.h>\n#include <GL/glx.h>\n\n// Conficts with core/optionparser.h\n#ifdef None\n#undef None\n#endif\n\n#endif\n\n#include <openxr/openxr.h>\n#include <openxr/openxr_platform.h>\n\n#include <glm/glm.hpp>\n\n#include \"maincontext.h\"\n\nclass XrBuddy\n{\npublic:\n    XrBuddy(MainContext& mainContextIn, const glm::vec2& nearFarIn);\n\n    bool Init();\n    bool PollEvents();\n    bool SyncInput();\n\n    using RenderCallback = std::function<void(const glm::mat4& projMat, const glm::mat4& eyeMat, const glm::vec4& viewport, const glm::vec2& nearFar, int32_t viewNum)>;\n    void SetRenderCallback(RenderCallback renderCallbackIn)\n    {\n        renderCallback = renderCallbackIn;\n    }\n    bool SessionReady() const;\n    bool RenderFrame();\n    bool Shutdown();\n\n    struct SwapchainInfo\n    {\n        XrSwapchain handle;\n        int32_t width;\n        int32_t height;\n    };\n\n    struct ActionInfo\n    {\n        ActionInfo() {}\n        ActionInfo(XrAction actionIn, XrActionType typeIn, XrSpace spaceIn) : action(actionIn), type(typeIn), space(spaceIn)\n        {\n            spaceLocation.type = XR_TYPE_SPACE_LOCATION;\n            spaceLocation.next = NULL;\n            spaceVelocity.type = XR_TYPE_SPACE_VELOCITY;\n            spaceVelocity.next = NULL;\n        }\n        XrAction action;\n        XrActionType type;\n        union\n        {\n            XrActionStateBoolean boolState;\n            XrActionStateFloat floatState;\n            XrActionStateVector2f vec2State;\n            XrActionStatePose poseState;\n        } u;\n        XrSpace space;\n        XrSpaceLocation spaceLocation;\n        XrSpaceVelocity spaceVelocity;\n    };\n\n    bool GetActionBool(const std::string& actionName, bool* value, bool* valid, bool* changed) const;\n    bool GetActionFloat(const std::string& actionName, float* value, bool* valid, bool* changed) const;\n    bool GetActionVec2(const std::string& actionName, glm::vec2* value, bool* valid, bool* changed) const;\n    bool GetActionPosition(const std::string& actionName, glm::vec3* value, bool* valid, bool* tracked) const;\n    bool GetActionOrientation(const std::string& actionName, glm::quat* value, bool* valid, bool* tracked) const;\n    bool GetActionLinearVelocity(const std::string& actionName, glm::vec3* value, bool* valid) const;\n    bool GetActionAngularVelocity(const std::string& actionName, glm::vec3* value, bool* valid) const;\n\n    uint32_t GetColorTexture() const;\n\n    void CycleColorSpace();\n\n#if defined(XR_USE_GRAPHICS_API_OPENGL)\n    using SwapchainImage = XrSwapchainImageOpenGLKHR;\n#elif defined(XR_USE_GRAPHICS_API_OPENGL_ES)\n    using SwapchainImage = XrSwapchainImageOpenGLESKHR;\n#endif\n\nprotected:\n    bool LocateSpaces(XrTime predictedDisplayTime);\n    bool RenderLayer(XrTime predictedDisplayTime,\n                     std::vector<XrCompositionLayerProjectionView>& projectionLayerViews,\n                     XrCompositionLayerProjection& layer);\n    void RenderView(const XrCompositionLayerProjectionView& layerView, uint32_t frameBuffer,\n                    uint32_t colorTexture, uint32_t depthTexture, int32_t viewNum);\n\n    bool constructorSucceded = false;\n    MainContext& mainContext;\n    XrSessionState state = XR_SESSION_STATE_UNKNOWN;\n    std::vector<XrExtensionProperties> extensionProps;\n    std::vector<XrApiLayerProperties> layerProps;\n    std::vector<XrViewConfigurationView> viewConfigs;\n\n    XrInstance instance = XR_NULL_HANDLE;\n    XrSystemId systemId = XR_NULL_SYSTEM_ID;\n    XrSystemProperties systemProperties = {};\n    XrSession session = XR_NULL_HANDLE;\n    XrActionSet actionSet = XR_NULL_HANDLE;\n\n    std::map<std::string, ActionInfo> actionMap;\n\n    XrSpace stageSpace = XR_NULL_HANDLE;\n    XrSpace viewSpace = XR_NULL_HANDLE;\n    XrSpaceLocation viewSpaceLocation = {};\n    XrSpaceVelocity viewSpaceVelocity = {};\n\n    std::vector<SwapchainInfo> swapchains;\n    std::vector<std::vector<SwapchainImage>> swapchainImages;\n\n    uint32_t frameBuffer = 0;\n    std::map<uint32_t, uint32_t> colorToDepthMap;\n    XrTime lastPredictedDisplayTime = 0;\n    uint32_t prevLastColorTexture = 0;\n    uint32_t lastColorTexture = 0;\n    bool sessionReady = false;\n\n    RenderCallback renderCallback;\n    glm::vec2 nearFar;\n};\n"
  },
  {
    "path": "src/flycam.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"flycam.h\"\n\n#include \"core/log.h\"\n#include \"core/util.h\"\n\nFlyCam::FlyCam(const glm::vec3& worldUpIn, const glm::vec3& posIn, const glm::quat& rotIn, float speedIn, float rotSpeedIn) :\n    worldUp(worldUpIn), pos(posIn), vel(0.0f, 0.0f, 0.0f), rot(rotIn),\n    cameraMat(MakeMat4(rot, pos)), speed(speedIn), rotSpeed(rotSpeedIn)\n{\n\n}\n\nvoid FlyCam::Process(const glm::vec2& leftStickIn, const glm::vec2& rightStickIn, float rollAmountIn,\n                     float upAmountIn, float dt)\n{\n    glm::vec2 leftStick = leftStickIn;\n    glm::vec2 rightStick = rightStickIn;\n    float rollAmount = rollAmountIn;\n\n    const float STIFF = 15.0f;\n    const float K = STIFF / speed;\n\n    // left stick controls position\n    glm::vec3 stick = rot * glm::vec3(leftStick.x, upAmountIn, -leftStick.y);\n    glm::vec3 s_over_k = (stick * STIFF) / K;\n    glm::vec3 s_over_k_sq = (stick * STIFF) / (K * K);\n    float e_neg_kt = exp(-K * dt);\n\n    glm::vec3 v = s_over_k + e_neg_kt * (vel - s_over_k);\n    pos = s_over_k * dt + (s_over_k_sq - vel / K) * e_neg_kt + pos - s_over_k_sq + (vel / K);\n    vel = v;\n\n    // right stick controls orientation\n    //glm::vec3 up = rot * glm::vec3(0.0f, 1.0f, 0.0f);\n    glm::vec3 right = rot * glm::vec3(1.0f, 0.0f, 0.0f);\n    glm::vec3 forward = rot * glm::vec3(0.0f, 0.0f, -1.0f);\n    glm::quat yaw = glm::angleAxis(rotSpeed * dt * -rightStick.x, worldUp);\n    glm::quat pitch = glm::angleAxis(rotSpeed * dt * rightStick.y, right);\n    rot = (yaw * pitch) * rot;\n\n    // axes of new cameraMat\n    glm::vec3 x = rot * glm::vec3(1.0f, 0.0f, 0.0f);\n    glm::vec3 y = rot * glm::vec3(0.0f, 1.0f, 0.0f);\n    glm::vec3 z = rot * glm::vec3(0.0f, 0.0f, 1.0f);\n\n    // apply roll to worldUp\n    if (fabs(rollAmountIn) > 0.1f)\n    {\n        worldUp = glm::vec3(cameraMat[1]);\n        glm::quat roll = glm::angleAxis(rotSpeed * dt * rollAmount, forward);\n        worldUp = roll * worldUp;\n    }\n\n    // make sure that cameraMat will be orthogonal, and aligned with world up.\n    if (glm::dot(z, worldUp) < 0.999f) // if w are aren't looking stright up.\n    {\n        glm::vec3 xx = glm::normalize(glm::cross(worldUp, z));\n        glm::vec3 yy = glm::normalize(glm::cross(z, xx));\n        cameraMat = glm::mat4(glm::vec4(xx, 0.0f), glm::vec4(yy, 0.0f), glm::vec4(z, 0.0f), glm::vec4(pos, 1.0f));\n    }\n    else\n    {\n        cameraMat = glm::mat4(glm::vec4(x, 0.0f), glm::vec4(y, 0.0f), glm::vec4(z, 0.0f), glm::vec4(pos, 1.0f));\n    }\n    glm::vec3 unusedScale;\n    Decompose(cameraMat, &unusedScale, &rot);\n}\n\nvoid FlyCam::SetCameraMat(const glm::mat4& cameraMat)\n{\n    pos = glm::vec3(cameraMat[3]);\n    rot = glm::normalize(glm::quat(glm::mat3(cameraMat)));\n    vel = glm::vec3(0.0f, 0.0f, 0.0f);\n}\n"
  },
  {
    "path": "src/flycam.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <glm/gtc/quaternion.hpp>\n\nclass FlyCam\n{\npublic:\n    FlyCam(const glm::vec3& worldUpIn, const glm::vec3& posIn, const glm::quat& rotIn, float speedIn, float rotSpeedIn);\n\n    void Process(const glm::vec2& leftStickIn, const glm::vec2& rightStickIn, float rollAmountIn,\n                 float upAmountIn, float dt);\n    const glm::mat4& GetCameraMat() const { return cameraMat; }\n    void SetCameraMat(const glm::mat4& cameraMat);\n\nprotected:\n\n    float speed;  // units per sec\n    float rotSpeed; // radians per sec\n    glm::vec3 worldUp;\n    glm::vec3 pos;\n    glm::vec3 vel;\n    glm::quat rot;\n    glm::mat4 cameraMat;\n};\n"
  },
  {
    "path": "src/gaussiancloud.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"gaussiancloud.h\"\n\n#include <algorithm>\n#include <cassert>\n#include <fstream>\n#include <iostream>\n#include <sstream>\n#include <string>\n#include <string.h>\n\n#include <glm/gtc/quaternion.hpp>\n\n#include <Eigen/Dense>\n\n#ifdef TRACY_ENABLE\n#include <tracy/Tracy.hpp>\n#else\n#define ZoneScoped\n#define ZoneScopedNC(NAME, COLOR)\n#endif\n\n#include \"core/log.h\"\n#include \"core/util.h\"\n\n#include \"ply.h\"\n\nstruct BaseGaussianData\n{\n    BaseGaussianData() noexcept {}\n    float posWithAlpha[4]; // center of the gaussian in object coordinates, with alpha in w\n    float r_sh0[4]; // sh coeff for red channel (up to third-order)\n    float g_sh0[4]; // sh coeff for green channel\n    float b_sh0[4];  // sh coeff for blue channel\n    float cov3_col0[3]; // 3x3 covariance matrix of the splat in object coordinates.\n    float cov3_col1[3];\n    float cov3_col2[3];\n};\n\nstruct FullGaussianData : public BaseGaussianData\n{\n    FullGaussianData() noexcept {}\n    float r_sh1[4];\n    float r_sh2[4];\n    float r_sh3[4];\n    float g_sh1[4];\n    float g_sh2[4];\n    float g_sh3[4];\n    float b_sh1[4];\n    float b_sh2[4];\n    float b_sh3[4];\n};\n\n// Function to convert glm::mat3 to Eigen::Matrix3f\nstatic Eigen::Matrix3f glmToEigen(const glm::mat3& glmMat)\n{\n    Eigen::Matrix3f eigenMat;\n    for (int r = 0; r < 3; ++r)\n    {\n        for (int c = 0; c < 3; ++c)\n        {\n            eigenMat(r, c) = glmMat[c][r];\n        }\n    }\n    return eigenMat;\n}\n\n// Function to convert Eigen::Matrix3f to glm::mat3\nstatic glm::mat3 eigenToGlm(const Eigen::Matrix3f& eigenMat)\n{\n    glm::mat3 glmMat;\n    for (int r = 0; r < 3; ++r)\n    {\n        for (int c = 0; c < 3; ++c)\n        {\n            glmMat[c][r] = eigenMat(r, c);\n        }\n    }\n    return glmMat;\n}\n\nstatic glm::mat3 ComputeCovMatFromRotScale(float rot[4], float scale[3])\n{\n    glm::quat q(rot[0], rot[1], rot[2], rot[3]);\n    glm::mat3 R(glm::normalize(q));\n    glm::mat3 S(glm::vec3(scale[0], 0.0f, 0.0f),\n                glm::vec3(0.0f, scale[1], 0.0f),\n                glm::vec3(0.0f, 0.0f, scale[2]));\n    return R * S * glm::transpose(S) * glm::transpose(R);\n}\n\nstatic void ComputeRotScaleFromCovMat(const glm::mat3& V, glm::quat& rotOut, glm::vec3& scaleOut)\n{\n    Eigen::Matrix3f eigenV = glmToEigen(V);\n\n    // Perform Eigen decomposition\n    Eigen::SelfAdjointEigenSolver<Eigen::Matrix3f> solver(eigenV);\n\n    // Get eigenvectors and eigenvalues\n    Eigen::Matrix3f eigenVec = solver.eigenvectors();\n    Eigen::Array3f eigenVal = solver.eigenvalues();\n    glm::mat3 R = eigenToGlm(eigenVec);\n    // glm mat3 to quat only works when det is 1.\n    if (glm::determinant(R) < 0)\n    {\n        R[0] *= -1.0f;\n        R[1] *= -1.0f;\n        R[2] *= -1.0f;\n    }\n    rotOut = glm::normalize(glm::quat(R));\n    // The eigenVal gives us the diagonal of (S*S^T), so take the sqrt to give is S.\n    scaleOut = glm::vec3(sqrtf(eigenVal(0)), sqrtf(eigenVal(1)), sqrtf(eigenVal(2)));\n}\n\nstatic float ComputeAlphaFromOpacity(float opacity)\n{\n    return 1.0f / (1.0f + expf(-opacity));\n}\n\nstatic float ComputeOpacityFromAlpha(float alpha)\n{\n    return -logf((1.0f / alpha) - 1.0f);\n}\n\nGaussianCloud::GaussianCloud(const Options& options) :\n    numGaussians(0),\n    gaussianSize(0),\n    opt(options),\n    hasFullSH(false)\n{\n    ;\n}\n\nbool GaussianCloud::ImportPly(const std::string& plyFilename)\n{\n    ZoneScopedNC(\"GC::ImportPly\", tracy::Color::Red4);\n\n    std::ifstream plyFile(plyFilename, std::ios::binary);\n    if (!plyFile.is_open())\n    {\n        Log::E(\"failed to open %s\\n\", plyFilename.c_str());\n        return false;\n    }\n\n    Ply ply;\n\n    {\n        ZoneScopedNC(\"ply.Parse\", tracy::Color::Blue);\n        if (!ply.Parse(plyFile))\n        {\n            Log::E(\"Error parsing ply file \\\"%s\\\"\\n\", plyFilename.c_str());\n            return false;\n        }\n    }\n\n    struct\n    {\n        BinaryAttribute x, y, z;\n        BinaryAttribute f_dc[3];\n        BinaryAttribute f_rest[45];\n        BinaryAttribute opacity;\n        BinaryAttribute scale[3];\n        BinaryAttribute rot[4];\n    } props;\n\n    {\n        ZoneScopedNC(\"ply.GetProps\", tracy::Color::Green);\n\n        if (!ply.GetProperty(\"x\", props.x) ||\n            !ply.GetProperty(\"y\", props.y) ||\n            !ply.GetProperty(\"z\", props.z))\n        {\n            Log::E(\"Error parsing ply file \\\"%s\\\", missing position property\\n\", plyFilename.c_str());\n        }\n\n        for (int i = 0; i < 3; i++)\n        {\n            if (!ply.GetProperty(\"f_dc_\" + std::to_string(i), props.f_dc[i]))\n            {\n                Log::E(\"Error parsing ply file \\\"%s\\\", missing f_dc property\\n\", plyFilename.c_str());\n            }\n        }\n\n        if (opt.importFullSH)\n        {\n            hasFullSH = true;\n            for (int i = 0; i < 45; i++)\n            {\n                if (!ply.GetProperty(\"f_rest_\" + std::to_string(i), props.f_rest[i]))\n                {\n                    // f_rest properties are optional\n                    Log::W(\"PLY file \\\"%s\\\", missing f_rest property\\n\", plyFilename.c_str());\n                    hasFullSH = false;\n                    break;\n                }\n            }\n        }\n        else\n        {\n            hasFullSH = false;\n        }\n\n        if (!ply.GetProperty(\"opacity\", props.opacity))\n        {\n            Log::E(\"Error parsing ply file \\\"%s\\\", missing opacity property\\n\", plyFilename.c_str());\n        }\n\n        for (int i = 0; i < 3; i++)\n        {\n            if (!ply.GetProperty(\"scale_\" + std::to_string(i), props.scale[i]))\n            {\n                Log::E(\"Error parsing ply file \\\"%s\\\", missing scale property\\n\", plyFilename.c_str());\n            }\n        }\n\n        for (int i = 0; i < 4; i++)\n        {\n            if (!ply.GetProperty(\"rot_\" + std::to_string(i), props.rot[i]))\n            {\n                Log::E(\"Error parsing ply file \\\"%s\\\", missing rot property\\n\", plyFilename.c_str());\n            }\n        }\n\n    }\n\n    InitAttribs();\n\n    {\n        ZoneScopedNC(\"alloc data\", tracy::Color::Red4);\n\n        numGaussians = ply.GetVertexCount();\n        if (hasFullSH)\n        {\n            gaussianSize = sizeof(FullGaussianData);\n            FullGaussianData* fullPtr = new FullGaussianData[numGaussians];\n            data.reset(fullPtr);\n        }\n        else\n        {\n            gaussianSize = sizeof(BaseGaussianData);\n            BaseGaussianData* basePtr = new BaseGaussianData[numGaussians];\n            data.reset(basePtr);\n        }\n    }\n\n    {\n        ZoneScopedNC(\"ply.ForEachVertex\", tracy::Color::Blue);\n        int i = 0;\n        uint8_t* rawPtr = (uint8_t*)data.get();\n        ply.ForEachVertex([this, &rawPtr, &i, &props](const void* plyData, size_t size)\n        {\n            BaseGaussianData* basePtr = reinterpret_cast<BaseGaussianData*>(rawPtr);\n            basePtr->posWithAlpha[0] = props.x.Read<float>(plyData);\n            basePtr->posWithAlpha[1] = props.y.Read<float>(plyData);\n            basePtr->posWithAlpha[2] = props.z.Read<float>(plyData);\n            basePtr->posWithAlpha[3] = ComputeAlphaFromOpacity(props.opacity.Read<float>(plyData));\n\n            if (hasFullSH)\n            {\n                FullGaussianData* fullPtr = reinterpret_cast<FullGaussianData*>(rawPtr);\n                fullPtr->r_sh0[0] = props.f_dc[0].Read<float>(plyData);\n                fullPtr->r_sh0[1] = props.f_rest[0].Read<float>(plyData);\n                fullPtr->r_sh0[2] = props.f_rest[1].Read<float>(plyData);\n                fullPtr->r_sh0[3] = props.f_rest[2].Read<float>(plyData);\n                fullPtr->r_sh1[0] = props.f_rest[3].Read<float>(plyData);\n                fullPtr->r_sh1[1] = props.f_rest[4].Read<float>(plyData);\n                fullPtr->r_sh1[2] = props.f_rest[5].Read<float>(plyData);\n                fullPtr->r_sh1[3] = props.f_rest[6].Read<float>(plyData);\n                fullPtr->r_sh2[0] = props.f_rest[7].Read<float>(plyData);\n                fullPtr->r_sh2[1] = props.f_rest[8].Read<float>(plyData);\n                fullPtr->r_sh2[2] = props.f_rest[9].Read<float>(plyData);\n                fullPtr->r_sh2[3] = props.f_rest[10].Read<float>(plyData);\n                fullPtr->r_sh3[0] = props.f_rest[11].Read<float>(plyData);\n                fullPtr->r_sh3[1] = props.f_rest[12].Read<float>(plyData);\n                fullPtr->r_sh3[2] = props.f_rest[13].Read<float>(plyData);\n                fullPtr->r_sh3[3] = props.f_rest[14].Read<float>(plyData);\n\n                fullPtr->g_sh0[0] = props.f_dc[1].Read<float>(plyData);\n                fullPtr->g_sh0[1] = props.f_rest[15].Read<float>(plyData);\n                fullPtr->g_sh0[2] = props.f_rest[16].Read<float>(plyData);\n                fullPtr->g_sh0[3] = props.f_rest[17].Read<float>(plyData);\n                fullPtr->g_sh1[0] = props.f_rest[18].Read<float>(plyData);\n                fullPtr->g_sh1[1] = props.f_rest[19].Read<float>(plyData);\n                fullPtr->g_sh1[2] = props.f_rest[20].Read<float>(plyData);\n                fullPtr->g_sh1[3] = props.f_rest[21].Read<float>(plyData);\n                fullPtr->g_sh2[0] = props.f_rest[22].Read<float>(plyData);\n                fullPtr->g_sh2[1] = props.f_rest[23].Read<float>(plyData);\n                fullPtr->g_sh2[2] = props.f_rest[24].Read<float>(plyData);\n                fullPtr->g_sh2[3] = props.f_rest[25].Read<float>(plyData);\n                fullPtr->g_sh3[0] = props.f_rest[26].Read<float>(plyData);\n                fullPtr->g_sh3[1] = props.f_rest[27].Read<float>(plyData);\n                fullPtr->g_sh3[2] = props.f_rest[28].Read<float>(plyData);\n                fullPtr->g_sh3[3] = props.f_rest[29].Read<float>(plyData);\n\n                fullPtr->b_sh0[0] = props.f_dc[2].Read<float>(plyData);\n                fullPtr->b_sh0[1] = props.f_rest[30].Read<float>(plyData);\n                fullPtr->b_sh0[2] = props.f_rest[31].Read<float>(plyData);\n                fullPtr->b_sh0[3] = props.f_rest[32].Read<float>(plyData);\n                fullPtr->b_sh1[0] = props.f_rest[33].Read<float>(plyData);\n                fullPtr->b_sh1[1] = props.f_rest[34].Read<float>(plyData);\n                fullPtr->b_sh1[2] = props.f_rest[35].Read<float>(plyData);\n                fullPtr->b_sh1[3] = props.f_rest[36].Read<float>(plyData);\n                fullPtr->b_sh2[0] = props.f_rest[37].Read<float>(plyData);\n                fullPtr->b_sh2[1] = props.f_rest[38].Read<float>(plyData);\n                fullPtr->b_sh2[2] = props.f_rest[39].Read<float>(plyData);\n                fullPtr->b_sh2[3] = props.f_rest[40].Read<float>(plyData);\n                fullPtr->b_sh3[0] = props.f_rest[41].Read<float>(plyData);\n                fullPtr->b_sh3[1] = props.f_rest[42].Read<float>(plyData);\n                fullPtr->b_sh3[2] = props.f_rest[43].Read<float>(plyData);\n                fullPtr->b_sh3[3] = props.f_rest[44].Read<float>(plyData);\n            }\n            else\n            {\n                basePtr->r_sh0[0] = props.f_dc[0].Read<float>(plyData);\n                basePtr->r_sh0[1] = 0.0f;\n                basePtr->r_sh0[2] = 0.0f;\n                basePtr->r_sh0[3] = 0.0f;\n\n                basePtr->g_sh0[0] = props.f_dc[1].Read<float>(plyData);\n                basePtr->g_sh0[1] = 0.0f;\n                basePtr->g_sh0[2] = 0.0f;\n                basePtr->g_sh0[3] = 0.0f;\n\n                basePtr->b_sh0[0] = props.f_dc[2].Read<float>(plyData);\n                basePtr->b_sh0[1] = 0.0f;\n                basePtr->b_sh0[2] = 0.0f;\n                basePtr->b_sh0[3] = 0.0f;\n            }\n\n            // NOTE: scale is stored in logarithmic scale in plyFile\n            float scale[3] =\n            {\n                expf(props.scale[0].Read<float>(plyData)),\n                expf(props.scale[1].Read<float>(plyData)),\n                expf(props.scale[2].Read<float>(plyData))\n            };\n            float rot[4] =\n            {\n                props.rot[0].Read<float>(plyData),\n                props.rot[1].Read<float>(plyData),\n                props.rot[2].Read<float>(plyData),\n                props.rot[3].Read<float>(plyData)\n            };\n\n            glm::mat3 V = ComputeCovMatFromRotScale(rot, scale);\n            basePtr->cov3_col0[0] = V[0][0];\n            basePtr->cov3_col0[1] = V[0][1];\n            basePtr->cov3_col0[2] = V[0][2];\n            basePtr->cov3_col1[0] = V[1][0];\n            basePtr->cov3_col1[1] = V[1][1];\n            basePtr->cov3_col1[2] = V[1][2];\n            basePtr->cov3_col2[0] = V[2][0];\n            basePtr->cov3_col2[1] = V[2][1];\n            basePtr->cov3_col2[2] = V[2][2];\n            i++;\n            rawPtr += gaussianSize;\n        });\n    }\n\n    return true;\n}\n\nbool GaussianCloud::ExportPly(const std::string& plyFilename) const\n{\n    std::ofstream plyFile(plyFilename, std::ios::binary);\n    if (!plyFile.is_open())\n    {\n        Log::E(\"failed to open %s\\n\", plyFilename.c_str());\n        return false;\n    }\n\n    Ply ply;\n    ply.AddProperty(\"x\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"y\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"z\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"nx\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"ny\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"nz\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"f_dc_0\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"f_dc_1\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"f_dc_2\", BinaryAttribute::Type::Float);\n    if (opt.exportFullSH)\n    {\n        for (int i = 0; i < 45; i++)\n        {\n            ply.AddProperty(\"f_rest_\" + std::to_string(i), BinaryAttribute::Type::Float);\n        }\n    }\n    ply.AddProperty(\"opacity\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"scale_0\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"scale_1\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"scale_2\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"rot_0\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"rot_1\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"rot_2\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"rot_3\", BinaryAttribute::Type::Float);\n\n    struct\n    {\n        BinaryAttribute x, y, z;\n        BinaryAttribute nx, ny, nz;\n        BinaryAttribute f_dc[3];\n        BinaryAttribute f_rest[45];\n        BinaryAttribute opacity;\n        BinaryAttribute scale[3];\n        BinaryAttribute rot[4];\n    } props;\n\n    ply.GetProperty(\"x\", props.x);\n    ply.GetProperty(\"y\", props.y);\n    ply.GetProperty(\"z\", props.z);\n    ply.GetProperty(\"nx\", props.nx);\n    ply.GetProperty(\"ny\", props.ny);\n    ply.GetProperty(\"nz\", props.nz);\n    ply.GetProperty(\"f_dc_0\", props.f_dc[0]);\n    ply.GetProperty(\"f_dc_1\", props.f_dc[1]);\n    ply.GetProperty(\"f_dc_2\", props.f_dc[2]);\n    if (opt.exportFullSH)\n    {\n        for (int i = 0; i < 45; i++)\n        {\n            ply.GetProperty(\"f_rest_\" + std::to_string(i), props.f_rest[i]);\n        }\n    }\n    ply.GetProperty(\"opacity\", props.opacity);\n    ply.GetProperty(\"scale_0\", props.scale[0]);\n    ply.GetProperty(\"scale_1\", props.scale[1]);\n    ply.GetProperty(\"scale_2\", props.scale[2]);\n    ply.GetProperty(\"rot_0\", props.rot[0]);\n    ply.GetProperty(\"rot_1\", props.rot[1]);\n    ply.GetProperty(\"rot_2\", props.rot[2]);\n    ply.GetProperty(\"rot_3\", props.rot[3]);\n\n    ply.AllocData(numGaussians);\n\n    uint8_t* gData = (uint8_t*)data.get();\n    size_t runningSize = 0;\n    ply.ForEachVertexMut([this, &props, &gData, &runningSize](void* plyData, size_t size)\n    {\n        const float* posWithAlpha = posWithAlphaAttrib.Get<float>(gData);\n        const float* r_sh0 = r_sh0Attrib.Get<float>(gData);\n        const float* g_sh0 = g_sh0Attrib.Get<float>(gData);\n        const float* b_sh0 = b_sh0Attrib.Get<float>(gData);\n        const float* cov3_col0 = cov3_col0Attrib.Get<float>(gData);\n\n        props.x.Write<float>(plyData, posWithAlpha[0]);\n        props.y.Write<float>(plyData, posWithAlpha[1]);\n        props.z.Write<float>(plyData, posWithAlpha[2]);\n        props.nx.Write<float>(plyData, 0.0f);\n        props.ny.Write<float>(plyData, 0.0f);\n        props.nz.Write<float>(plyData, 0.0f);\n        props.f_dc[0].Write<float>(plyData, r_sh0[0]);\n        props.f_dc[1].Write<float>(plyData, g_sh0[0]);\n        props.f_dc[2].Write<float>(plyData, b_sh0[0]);\n\n        if (opt.exportFullSH)\n        {\n            // TODO: maybe just a raw memcopy would be faster\n            for (int i = 0; i < 15; i++)\n            {\n                props.f_rest[i].Write<float>(plyData, r_sh0[i + 1]);\n            }\n            for (int i = 0; i < 15; i++)\n            {\n                props.f_rest[i + 15].Write<float>(plyData, g_sh0[i + 1]);\n            }\n            for (int i = 0; i < 15; i++)\n            {\n                props.f_rest[i + 30].Write<float>(plyData, b_sh0[i + 1]);\n            }\n        }\n\n        props.opacity.Write<float>(plyData, ComputeOpacityFromAlpha(posWithAlpha[3]));\n\n        glm::mat3 V(cov3_col0[0], cov3_col0[1], cov3_col0[2],\n                    cov3_col0[3], cov3_col0[4], cov3_col0[5],\n                    cov3_col0[6], cov3_col0[7], cov3_col0[8]);\n\n        glm::quat rot;\n        glm::vec3 scale;\n        ComputeRotScaleFromCovMat(V, rot, scale);\n\n        props.scale[0].Write<float>(plyData, logf(scale.x));\n        props.scale[1].Write<float>(plyData, logf(scale.y));\n        props.scale[2].Write<float>(plyData, logf(scale.z));\n        props.rot[0].Write<float>(plyData, rot.w);\n        props.rot[1].Write<float>(plyData, rot.x);\n        props.rot[2].Write<float>(plyData, rot.y);\n        props.rot[3].Write<float>(plyData, rot.z);\n\n        gData += gaussianSize;\n        runningSize += gaussianSize;\n        assert(runningSize <= GetTotalSize());  // bad, we went off the end of the data ptr\n    });\n\n    ply.Dump(plyFile);\n\n    return true;\n}\n\nvoid GaussianCloud::InitDebugCloud()\n{\n    const int NUM_SPLATS = 5;\n\n    numGaussians = NUM_SPLATS * 3 + 1;\n    gaussianSize = sizeof(FullGaussianData);\n    InitAttribs();\n    FullGaussianData* gd = new FullGaussianData[numGaussians];\n    data.reset(gd);\n\n    //\n    // make an debug GaussianClound, that contain red, green and blue axes.\n    //\n    const float AXIS_LENGTH = 1.0f;\n    const float DELTA = (AXIS_LENGTH / (float)NUM_SPLATS);\n    const float COV_DIAG = 0.005f;\n    const float SH_C0 = 0.28209479177387814f;\n    const float SH_ONE = 1.0f / (2.0f * SH_C0);\n    const float SH_ZERO = -1.0f / (2.0f * SH_C0);\n\n    // x axis\n    for (int i = 0; i < NUM_SPLATS; i++)\n    {\n        FullGaussianData g;\n        memset(&g, 0, sizeof(FullGaussianData));\n        g.posWithAlpha[0] = i * DELTA + DELTA;\n        g.posWithAlpha[1] = 0.0f;\n        g.posWithAlpha[2] = 0.0f;\n        g.posWithAlpha[3] = 1.0f;\n        // red\n        g.r_sh0[0] = SH_ONE; g.g_sh0[0] = SH_ZERO; g.b_sh0[0] = SH_ZERO;\n        g.cov3_col0[0] = COV_DIAG; g.cov3_col1[1] = COV_DIAG; g.cov3_col2[2] = COV_DIAG;\n        gd[i] = g;\n    }\n    // y axis\n    for (int i = 0; i < NUM_SPLATS; i++)\n    {\n        FullGaussianData g;\n        memset(&g, 0, sizeof(FullGaussianData));\n        g.posWithAlpha[0] = 0.0f;\n        g.posWithAlpha[1] = i * DELTA + DELTA;\n        g.posWithAlpha[2] = 0.0f;\n        g.posWithAlpha[3] = 1.0f;\n        // green\n        g.r_sh0[0] = SH_ZERO; g.g_sh0[0] = SH_ONE; g.b_sh0[0] = SH_ZERO;\n        g.cov3_col0[0] = COV_DIAG; g.cov3_col1[1] = COV_DIAG; g.cov3_col2[2] = COV_DIAG;\n        gd[NUM_SPLATS + i] = g;\n    }\n    // z axis\n    for (int i = 0; i < NUM_SPLATS; i++)\n    {\n        FullGaussianData g;\n        memset(&g, 0, sizeof(FullGaussianData));\n        g.posWithAlpha[0] = 0.0f;\n        g.posWithAlpha[1] = 0.0f;\n        g.posWithAlpha[2] = i * DELTA + DELTA + 0.0001f; // AJT: HACK prevent div by zero for debug-shaders\n        g.posWithAlpha[3] = 1.0f;\n        // blue\n        g.r_sh0[0] = SH_ZERO; g.g_sh0[0] = SH_ZERO; g.b_sh0[0] = SH_ONE;\n        g.cov3_col0[0] = COV_DIAG; g.cov3_col1[1] = COV_DIAG; g.cov3_col2[2] = COV_DIAG;\n        gd[(NUM_SPLATS * 2) + i] = g;\n    }\n\n    FullGaussianData g;\n    memset(&g, 0, sizeof(FullGaussianData));\n    g.posWithAlpha[0] = 0.0f;\n    g.posWithAlpha[1] = 0.0f;\n    g.posWithAlpha[2] = 0.0f;\n    g.posWithAlpha[3] = 1.0f;\n    // white\n    g.r_sh0[0] = SH_ONE; g.g_sh0[0] = SH_ONE; g.b_sh0[0] = SH_ONE;\n    g.cov3_col0[0] = COV_DIAG; g.cov3_col1[1] = COV_DIAG; g.cov3_col2[2] = COV_DIAG;\n    gd[(NUM_SPLATS * 3)] = g;\n}\n\n// only keep the nearest splats\nvoid GaussianCloud::PruneSplats(const glm::vec3& origin, uint32_t numSplats)\n{\n    if (!data || static_cast<size_t>(numSplats) >= numGaussians)\n    {\n        return;\n    }\n\n    using IndexDistPair = std::pair<uint32_t, float>;\n    std::vector<IndexDistPair> indexDistVec;\n    indexDistVec.reserve(numGaussians);\n    uint8_t* rawPtr = (uint8_t*)data.get();\n    for (uint32_t i = 0; i < numGaussians; i++)\n    {\n        BaseGaussianData* basePtr = reinterpret_cast<BaseGaussianData*>(rawPtr);\n        glm::vec3 pos(basePtr->posWithAlpha[0], basePtr->posWithAlpha[1], basePtr->posWithAlpha[2]);\n        indexDistVec.push_back(IndexDistPair(i, glm::distance(origin, pos)));\n        rawPtr += gaussianSize;\n    }\n\n    std::sort(indexDistVec.begin(), indexDistVec.end(), [](const IndexDistPair& a, const IndexDistPair& b)\n    {\n        return a.second < b.second;\n    });\n\n    uint8_t* newData;\n    if (hasFullSH)\n    {\n        FullGaussianData* fullPtr = new FullGaussianData[numSplats];\n        newData = (uint8_t*)fullPtr;\n    }\n    else\n    {\n        BaseGaussianData* basePtr = new BaseGaussianData[numSplats];\n        newData = (uint8_t*)basePtr;\n    }\n    rawPtr = (uint8_t*)data.get();\n    uint8_t* rawPtr2 = newData;\n\n    for (uint32_t i = 0; i < numSplats; i++)\n    {\n        memcpy(rawPtr2, rawPtr + indexDistVec[i].first * gaussianSize, gaussianSize);\n        rawPtr2 += gaussianSize;\n    }\n    numGaussians = numSplats;\n    data.reset(newData);\n}\n\nvoid GaussianCloud::ForEachPosWithAlpha(const ForEachPosWithAlphaCallback& cb) const\n{\n    posWithAlphaAttrib.ForEach<float>(GetRawDataPtr(), GetStride(), GetNumGaussians(), cb);\n}\n\nvoid GaussianCloud::InitAttribs()\n{\n    // BaseGaussianData attribs\n    posWithAlphaAttrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, posWithAlpha)};\n    r_sh0Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, r_sh0)};\n    g_sh0Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, g_sh0)};\n    b_sh0Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, b_sh0)};\n    cov3_col0Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, cov3_col0)};\n    cov3_col1Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, cov3_col1)};\n    cov3_col2Attrib = {BinaryAttribute::Type::Float, offsetof(BaseGaussianData, cov3_col2)};\n\n    // FullGaussianData attribs\n    if (hasFullSH)\n    {\n        r_sh1Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, r_sh1)};\n        r_sh2Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, r_sh2)};\n        r_sh3Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, r_sh3)};\n        g_sh1Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, g_sh1)};\n        g_sh2Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, g_sh2)};\n        g_sh3Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, g_sh3)};\n        b_sh1Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, b_sh1)};\n        b_sh2Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, b_sh2)};\n        b_sh3Attrib = {BinaryAttribute::Type::Float, offsetof(FullGaussianData, b_sh3)};\n    }\n}\n"
  },
  {
    "path": "src/gaussiancloud.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <functional>\n#include <memory>\n#include <string>\n#include <vector>\n\n#include <glm/glm.hpp>\n\n#include \"core/binaryattribute.h\"\n\nclass GaussianCloud\n{\npublic:\n    struct Options\n    {\n        bool importFullSH;\n        bool exportFullSH;\n    };\n\n    GaussianCloud(const Options& options);\n\n    bool ImportPly(const std::string& plyFilename);\n    bool ExportPly(const std::string& plyFilename) const;\n\n    void InitDebugCloud();\n\n    // only keep the nearest splats\n    void PruneSplats(const glm::vec3& origin, uint32_t numGaussians);\n\n    size_t GetNumGaussians() const { return numGaussians; }\n    size_t GetStride() const { return gaussianSize; }\n    size_t GetTotalSize() const { return GetNumGaussians() * gaussianSize; }\n    void* GetRawDataPtr() { return data.get(); }\n    const void* GetRawDataPtr() const { return data.get(); }\n\n    const BinaryAttribute& GetPosWithAlphaAttrib() const { return posWithAlphaAttrib; }\n    const BinaryAttribute& GetR_SH0Attrib() const { return r_sh0Attrib; }\n    const BinaryAttribute& GetR_SH1Attrib() const { return r_sh1Attrib; }\n    const BinaryAttribute& GetR_SH2Attrib() const { return r_sh2Attrib; }\n    const BinaryAttribute& GetR_SH3Attrib() const { return r_sh3Attrib; }\n    const BinaryAttribute& GetG_SH0Attrib() const { return g_sh0Attrib; }\n    const BinaryAttribute& GetG_SH1Attrib() const { return g_sh1Attrib; }\n    const BinaryAttribute& GetG_SH2Attrib() const { return g_sh2Attrib; }\n    const BinaryAttribute& GetG_SH3Attrib() const { return g_sh3Attrib; }\n    const BinaryAttribute& GetB_SH0Attrib() const { return b_sh0Attrib; }\n    const BinaryAttribute& GetB_SH1Attrib() const { return b_sh1Attrib; }\n    const BinaryAttribute& GetB_SH2Attrib() const { return b_sh2Attrib; }\n    const BinaryAttribute& GetB_SH3Attrib() const { return b_sh3Attrib; }\n    const BinaryAttribute& GetCov3_Col0Attrib() const { return cov3_col0Attrib; }\n    const BinaryAttribute& GetCov3_Col1Attrib() const { return cov3_col1Attrib; }\n    const BinaryAttribute& GetCov3_Col2Attrib() const { return cov3_col2Attrib; }\n\n    using ForEachPosWithAlphaCallback = std::function<void(const float*)>;\n    void ForEachPosWithAlpha(const ForEachPosWithAlphaCallback& cb) const;\n\n    bool HasFullSH() const { return hasFullSH; }\n\nprotected:\n    void InitAttribs();\n\n    std::shared_ptr<void> data;\n\n    BinaryAttribute posWithAlphaAttrib;\n    BinaryAttribute r_sh0Attrib;\n    BinaryAttribute r_sh1Attrib;\n    BinaryAttribute r_sh2Attrib;\n    BinaryAttribute r_sh3Attrib;\n    BinaryAttribute g_sh0Attrib;\n    BinaryAttribute g_sh1Attrib;\n    BinaryAttribute g_sh2Attrib;\n    BinaryAttribute g_sh3Attrib;\n    BinaryAttribute b_sh0Attrib;\n    BinaryAttribute b_sh1Attrib;\n    BinaryAttribute b_sh2Attrib;\n    BinaryAttribute b_sh3Attrib;\n    BinaryAttribute cov3_col0Attrib;\n    BinaryAttribute cov3_col1Attrib;\n    BinaryAttribute cov3_col2Attrib;\n\n    size_t numGaussians;\n    size_t gaussianSize;\n\n    Options opt;\n    bool hasFullSH;\n};\n"
  },
  {
    "path": "src/magiccarpet.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"magiccarpet.h\"\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#else\n#include <GL/glew.h>\n#endif\n\n#include <array>\n#include <glm/gtc/matrix_transform.hpp>\n\n#include \"core/image.h\"\n#include \"core/log.h\"\n#include \"core/util.h\"\n\nconst float SNAP_TIME = 1.0f;\nconst float SNAP_ANGLE = glm::radians(30.0f);\n\nconst float DOUBLE_GRIP_TIME = 0.1f;\n\nconst float CARPET_RADIUS = 3.0f;\nconst float CARPET_TILE_COUNT = 3.0f;\n\nglm::mat4 MagicCarpet::Pose::GetMat() const\n{\n    return MakeMat4(rot, pos);\n}\n\nvoid MagicCarpet::Pose::Dump(const std::string& name) const\n{\n    PrintVec(pos, name + \".pos\");\n    Log::D(\"%s.posValid = %s, %s.posTracked = %s\\n\", name.c_str(), posValid ? \"true\" : \"false\", name.c_str(), posTracked ? \"true\" : \"false\");\n    PrintQuat(rot, name + \".rot\");\n    Log::D(\"%s.rotValid = %s, %s.rotTracked = %s\\n\", name.c_str(), rotValid ? \"true\" : \"false\", name.c_str(), rotTracked ? \"true\" : \"false\");\n}\n\nMagicCarpet::MagicCarpet(const glm::mat4& carpetMatIn, float moveSpeedIn) :\n    sm(State::Normal),\n    carpetMat(carpetMatIn),\n    moveSpeed(moveSpeedIn)\n{\n    // Normal\n    sm.AddState(State::Normal, \"Normal\",\n                [this]() { snapTimer = 0.0f; },  // enter\n                [this]() {}, // exit\n                [this](float dt) { NormalProcess(dt); });  // process\n    sm.AddTransition(State::Normal, State::LeftGrip, \"leftGrip down\", [this]()\n    {\n        return in.buttonState.leftGrip;\n    });\n    sm.AddTransition(State::Normal, State::RightGrip, \"rightGrip down\", [this]()\n    {\n        return in.buttonState.rightGrip;\n    });\n\n    // LeftGrip\n    sm.AddState(State::LeftGrip, \"LeftGrip\",\n                [this]() { GrabPoses(); },\n                [this]() {},\n                [this](float dt)\n                {\n                    if (GripCount() == 1)\n                    {\n                        gripTimer = DOUBLE_GRIP_TIME;\n                    }\n                    gripTimer -= dt;\n                    glm::mat4 grabMat = MakeMat4(grabLeftPose.rot, grabLeftPose.pos);\n                    glm::vec3 pos = in.leftPose.pos;\n                    glm::quat rot = grabLeftPose.rot;\n                    glm::mat4 currMat = MakeMat4(rot, pos);\n\n                    // adjust the carpet mat\n                    carpetMat = grabCarpetMat * grabMat * glm::inverse(currMat);\n                });\n    sm.AddTransition(State::LeftGrip, State::Normal, \"leftGrip up\", [this]()\n    {\n        return !in.buttonState.leftGrip;\n    });\n    sm.AddTransition(State::LeftGrip, State::DoubleGrip, \"double grip\", [this]()\n    {\n        return GripCount() == 2 && gripTimer < 0.0f;\n    });\n\n    // RightGrip\n    sm.AddState(State::RightGrip, \"RightGrip\",\n                [this]() { GrabPoses(); },\n                [this]() {},\n                [this](float dt)\n                {\n                    if (GripCount() == 1)\n                    {\n                        gripTimer = DOUBLE_GRIP_TIME;\n                    }\n                    gripTimer -= dt;\n                    glm::mat4 grabMat = MakeMat4(grabRightPose.rot, grabRightPose.pos);\n                    glm::vec3 pos = in.rightPose.pos;\n                    glm::quat rot = grabRightPose.rot;\n                    glm::mat4 currMat = MakeMat4(rot, pos);\n\n                    // adjust the carpet mat\n                    carpetMat = grabCarpetMat * grabMat * glm::inverse(currMat);\n                });\n    sm.AddTransition(State::RightGrip, State::Normal, \"rightGrip up\", [this]()\n    {\n        return !in.buttonState.rightGrip;\n    });\n    sm.AddTransition(State::RightGrip, State::DoubleGrip, \"double grip\", [this]()\n    {\n        return GripCount() == 2 && gripTimer < 0.0f;\n    });\n\n    // DoubleGrip\n    sm.AddState(State::DoubleGrip, \"DoubleGrip\",\n                [this]() { GrabPoses(); scaleMode = (TriggerCount() > 0); },\n                [this]() {},\n                [this](float dt)\n                {\n                    glm::vec3 d0 = grabRightPose.pos - grabLeftPose.pos;\n                    glm::vec3 p0 = glm::mix(grabLeftPose.pos, grabRightPose.pos, 0.5f);\n                    glm::vec3 x0 = SafeNormalize(d0, glm::vec3(1.0f, 0.0f, 0.0f));\n                    glm::vec3 y0 =  glm::vec3(0.0f, 1.0f, 0.0f);\n                    glm::vec3 z0 = glm::normalize(glm::cross(x0, y0));\n                    y0 = glm::normalize(glm::cross(z0, x0));\n                    glm::mat4 grabMat(glm::vec4(x0, 0.0f), glm::vec4(y0, 0.0f), glm::vec4(z0, 0.0f), glm::vec4(p0, 1.0f));\n\n                    glm::vec3 d1 = in.rightPose.pos - in.leftPose.pos;\n                    glm::vec3 p1 = glm::mix(in.leftPose.pos, in.rightPose.pos, 0.5f);\n                    glm::vec3 x1 = SafeNormalize(d1, glm::vec3(1.0f, 0.0f, 0.0f));\n                    glm::vec3 y1 = glm::vec3(0.0f, 1.0f, 0.0f);\n                    glm::vec3 z1 = glm::normalize(glm::cross(x1, y1));\n                    y1 = glm::normalize(glm::cross(z1, x1));\n                    glm::mat4 currMat(glm::vec4(x1, 0.0f), glm::vec4(y1, 0.0f), glm::vec4(z1, 0.0f), glm::vec4(p1, 1.0f));\n                    float s1 = glm::length(d1) / glm::length(d0);\n\n                    if (scaleMode)\n                    {\n                        currMat *= MakeMat4(s1, glm::quat(1.0f, 0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f));\n                    }\n\n                    // adjust the carpet mat\n                    carpetMat = grabCarpetMat * grabMat * glm::inverse(currMat);\n                });\n    sm.AddTransition(State::DoubleGrip, State::LeftGrip, \"grip count 1\", [this]()\n    {\n        return GripCount() == 1 && !in.buttonState.rightGrip;\n    });\n    sm.AddTransition(State::DoubleGrip, State::RightGrip, \"grip count 1\", [this]()\n    {\n        return GripCount() == 1 && !in.buttonState.leftGrip;\n    });\n    sm.AddTransition(State::DoubleGrip, State::Normal, \"grip count 0\", [this]()\n    {\n        return GripCount() == 0;\n    });\n}\n\nbool MagicCarpet::Init(bool isFramebufferSRGBEnabledIn)\n{\n    isFramebufferSRGBEnabled = isFramebufferSRGBEnabledIn;\n\n    Image carpetImg;\n    if (!carpetImg.Load(\"texture/carpet.png\"))\n    {\n        Log::E(\"Error loading carpet.png\\n\");\n        return false;\n    }\n    carpetImg.isSRGB = isFramebufferSRGBEnabledIn;\n    Texture::Params texParams = {FilterType::LinearMipmapLinear, FilterType::Linear, WrapType::Repeat, WrapType::Repeat};\n    carpetTex = std::make_shared<Texture>(carpetImg, texParams);\n\n    carpetProg = std::make_shared<Program>();\n    if (!carpetProg->LoadVertFrag(\"shader/carpet_vert.glsl\", \"shader/carpet_frag.glsl\"))\n    {\n        Log::E(\"Error loading carpet shaders!\\n\");\n        return false;\n    }\n\n    carpetVao = std::make_shared<VertexArrayObject>();\n    std::vector<glm::vec3> posVec = {\n        glm::vec3(-CARPET_RADIUS, 0.0f, -CARPET_RADIUS),\n        glm::vec3(CARPET_RADIUS, 0.0f, -CARPET_RADIUS),\n        glm::vec3(CARPET_RADIUS, 0.0f, CARPET_RADIUS),\n        glm::vec3(-CARPET_RADIUS, 0.0f, CARPET_RADIUS)\n    };\n    auto posBuffer = std::make_shared<BufferObject>(GL_ARRAY_BUFFER, posVec);\n    std::vector<glm::vec2> uvVec = {\n        glm::vec2(0.0f, 0.0f),\n        glm::vec2(CARPET_TILE_COUNT, 0.0f),\n        glm::vec2(CARPET_TILE_COUNT, CARPET_TILE_COUNT),\n        glm::vec2(0.0f, CARPET_TILE_COUNT)\n    };\n    auto uvBuffer = std::make_shared<BufferObject>(GL_ARRAY_BUFFER, uvVec);\n\n    // build element array\n    std::vector<uint32_t> indexVec = {\n        0, 2, 1,\n        0, 3, 2\n    };\n    auto indexBuffer = std::make_shared<BufferObject>(GL_ELEMENT_ARRAY_BUFFER, indexVec);\n\n    // setup vertex array object with buffers\n    carpetVao->SetAttribBuffer(carpetProg->GetAttribLoc(\"position\"), posBuffer);\n    carpetVao->SetAttribBuffer(carpetProg->GetAttribLoc(\"uv\"), uvBuffer);\n    carpetVao->SetElementBuffer(indexBuffer);\n\n    return true;\n}\n\nvoid MagicCarpet::Process(const Pose& headPose, const Pose& leftPose, const Pose& rightPose,\n                          const glm::vec2& leftStick, const glm::vec2& rightStick,\n                          const ButtonState& buttonState, float dt)\n{\n    /*\n    glm::mat4 leftMat = carpetMat * leftPose.GetMat();\n    glm::mat4 rightMat = carpetMat * rightPose.GetMat();\n    DebugDraw_Transform(leftMat);\n    DebugDraw_Transform(rightMat);\n    */\n\n    in.headPose = headPose;\n    in.leftPose = leftPose;\n    in.rightPose = rightPose;\n    in.leftStick = leftStick;\n    in.rightStick = rightStick;\n    in.buttonState = buttonState;\n\n    sm.Process(dt);\n}\n\nvoid MagicCarpet::SetCarpetMat(const glm::mat4& carpetMatIn)\n{\n    carpetMat = carpetMatIn;\n}\n\nvoid MagicCarpet::Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                         const glm::vec4& viewport, const glm::vec2& nearFar)\n{\n    carpetProg->Bind();\n\n    glm::mat4 modelViewMat = glm::inverse(cameraMat) * carpetMat;\n    carpetProg->SetUniform(\"modelViewProjMat\", projMat * modelViewMat);\n    glActiveTexture(GL_TEXTURE0);\n    glBindTexture(GL_TEXTURE_2D, carpetTex->texture);\n    carpetProg->SetUniform(\"colorTex\", 0);\n    carpetVao->DrawElements(GL_TRIANGLES);\n}\n\nvoid MagicCarpet::NormalProcess(float dt)\n{\n    glm::vec3 horizVel;\n    if (in.headPose.rotValid)\n    {\n        // get the forward and right vectors of the HMD\n        glm::vec3 headForward = in.headPose.rot * glm::vec3(0.0f, 0.0f, -1.0f);\n        glm::vec3 headRight = in.headPose.rot * glm::vec3(1.0f, 0.0f, 0.0f);\n\n        // project the HMD forward & right vectors onto the carpet, i.e. make sure they lie in the horizontal plane\n        // AJT: TODO: Handle bad normalize\n        glm::vec3 horizForward = glm::normalize(glm::vec3(headForward.x, 0.0f, headForward.z));\n        glm::vec3 horizRight = glm::normalize(glm::vec3(headRight.x, 0.0f, headRight.z));\n\n        // use leftStick to move horizontally\n        horizVel = horizForward * in.leftStick.y * moveSpeed + horizRight * in.leftStick.x * moveSpeed;\n    }\n    else\n    {\n        horizVel = glm::vec3(0.0f, 0.0f, 0.0f);\n    }\n\n    // handle snap turns\n    snapTimer -= dt;\n    if (fabs(in.rightStick.x) > 0.5f && snapTimer < 0.0f && in.headPose.posValid && in.headPose.posTracked)\n    {\n        // snap!\n        float snapSign = in.rightStick.x > 0.0f ? -1.0f : 1.0f;\n\n        // Rotate the carpet around the users HMD\n        glm::vec3 snapPos = XformPoint(carpetMat, in.headPose.pos);\n        glm::quat snapRot = glm::angleAxis(snapSign * SNAP_ANGLE, glm::normalize(XformVec(carpetMat, glm::vec3(0.0f, 1.0f, 0.0f))));\n        carpetMat = MakeRotateAboutPointMat(snapPos, snapRot) * carpetMat;\n\n        snapTimer = SNAP_TIME;\n    }\n    else if (fabs(in.rightStick.x) < 0.2f)\n    {\n        // reset snap\n        snapTimer = 0.0f;\n    }\n\n    // move the carpet!\n    glm::vec3 vel = XformVec(carpetMat, horizVel);\n    carpetMat[3][0] += vel.x * dt;\n    carpetMat[3][1] += vel.y * dt;\n    carpetMat[3][2] += vel.z * dt;\n}\n\nvoid MagicCarpet::GrabPoses()\n{\n    grabLeftPose = in.leftPose;\n    grabRightPose = in.rightPose;\n    grabCarpetMat = carpetMat;\n}\n\nint MagicCarpet::GripCount() const\n{\n    int count = 0;\n    count += (in.buttonState.leftGrip) ? 1 : 0;\n    count += (in.buttonState.rightGrip) ? 1 : 0;\n    return count;\n}\n\nint MagicCarpet::TriggerCount() const\n{\n    int count = 0;\n    count += (in.buttonState.leftTrigger) ? 1 : 0;\n    count += (in.buttonState.rightTrigger) ? 1 : 0;\n    return count;\n}\n"
  },
  {
    "path": "src/magiccarpet.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <glm/gtc/quaternion.hpp>\n#include <memory>\n#include <string>\n\n#include \"core/program.h\"\n#include \"core/statemachine.h\"\n#include \"core/texture.h\"\n#include \"core/vertexbuffer.h\"\n\n// VR flycam\nclass MagicCarpet\n{\npublic:\n    MagicCarpet(const glm::mat4& carpetMatIn, float moveSpeedIn);\n\n    bool Init(bool isFramebufferSRGBEnabledIn);\n\n    struct Pose\n    {\n        Pose() : pos(), rot(), posValid(false), posTracked(false), rotValid(false), rotTracked(false) {}\n        glm::mat4 GetMat() const;\n        void Dump(const std::string& name) const;\n        glm::vec3 pos;\n        glm::quat rot;\n        bool posValid;\n        bool posTracked;\n        bool rotValid;\n        bool rotTracked;\n    };\n\n    struct ButtonState\n    {\n        ButtonState() : leftTrigger(false), rightTrigger(false), leftGrip(false), rightGrip(false) {}\n        bool leftTrigger;\n        bool rightTrigger;\n        bool leftGrip;\n        bool rightGrip;\n    };\n\n    void Process(const Pose& headPose, const Pose& leftPose, const Pose& rightPose,\n                 const glm::vec2& leftStick, const glm::vec2& rightStick,\n                 const ButtonState& buttonState, float dt);\n\n    const glm::mat4& GetCarpetMat() const { return carpetMat; }\n    void SetCarpetMat(const glm::mat4& carpetMatIn);\n\n    void Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                const glm::vec4& viewport, const glm::vec2& nearFar);\n\nprotected:\n    void NormalProcess(float dt);\n    void GrabPoses();\n    int GripCount() const;\n    int TriggerCount() const;\n\n    enum class State { Normal, LeftGrip, RightGrip, DoubleGrip };\n\n    struct InputContext\n    {\n        Pose headPose;\n        Pose leftPose;\n        Pose rightPose;\n        glm::vec2 leftStick;\n        glm::vec2 rightStick;\n        ButtonState buttonState;\n    };\n\n    float moveSpeed;\n\n    StateMachine<State> sm;\n\n    InputContext in;\n\n    // used in normal state to perform snap turns\n    float snapTimer;\n\n    // used in grip states\n    float gripTimer;\n\n    // used in double grip state\n    bool scaleMode;\n\n    // used in grab states to store the pos/rot of controllers on entry into the state.\n    Pose grabLeftPose;\n    Pose grabRightPose;\n    glm::mat4 grabCarpetMat;\n\n    glm::mat4 carpetMat;\n\n    std::shared_ptr<Texture> carpetTex;\n    std::shared_ptr<Program> carpetProg;\n    std::shared_ptr<VertexArrayObject> carpetVao;\n    bool isFramebufferSRGBEnabled;\n};\n"
  },
  {
    "path": "src/maincontext.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#if defined(__ANDROID__)\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#include <jni.h>\n#include <android_native_app_glue.h>\n#elif defined(__linux__)\n#include <X11/Xlib.h>\n#include <X11/Xatom.h>\n#include <X11/extensions/xf86vmode.h>  // for fullscreen video mode\n#include <X11/extensions/Xrandr.h>     // for resolution changes\n#include <GL/glew.h>\n#include <GL/glx.h>\n\n// Conficts with core/optionparser.h\n#ifdef None\n#undef None\n#endif\n\n#endif\n\n#if defined(__ANDROID__)\n    struct MainContext\n    {\n        EGLDisplay display;\n        EGLConfig config;\n        EGLContext context;\n        android_app* androidApp;\n    };\n#elif defined(__linux__)\n    struct MainContext\n    {\n        Display* xdisplay;\n        uint32_t visualid;\n        GLXFBConfig glxFBConfig;\n        GLXDrawable glxDrawable;\n        GLXContext glxContext;\n    };\n\n#else\n    struct MainContext\n    {\n    };\n#endif\n\n"
  },
  {
    "path": "src/ply.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"ply.h\"\n\n#include <algorithm>\n#include <fstream>\n#include <iostream>\n#include <sstream>\n\n#ifdef TRACY_ENABLE\n#include <tracy/Tracy.hpp>\n#else\n#define ZoneScoped\n#define ZoneScopedNC(NAME, COLOR)\n#endif\n\n#include \"core/log.h\"\n\nstatic bool CheckLine(std::ifstream& plyFile, const std::string& validLine)\n{\n    std::string line;\n    return std::getline(plyFile, line) && line == validLine;\n}\n\nstatic bool GetNextPlyLine(std::ifstream& plyFile, std::string& lineOut)\n{\n    while (std::getline(plyFile, lineOut))\n    {\n        // skip comment lines\n        if (lineOut.find(\"comment\", 0) != 0)\n        {\n            return true;\n        }\n    }\n    return false;\n}\n\nstatic const char* BinaryAttributeTypeToString(BinaryAttribute::Type type)\n{\n    switch (type)\n    {\n    case BinaryAttribute::Type::Char:\n        return \"char\";\n    case BinaryAttribute::Type::UChar:\n        return \"uchar\";\n    case BinaryAttribute::Type::Short:\n        return \"short\";\n    case BinaryAttribute::Type::UShort:\n        return \"ushort\";\n    case BinaryAttribute::Type::Int:\n        return \"int\";\n    case BinaryAttribute::Type::UInt:\n        return \"uint\";\n    case BinaryAttribute::Type::Float:\n        return \"float\";\n    case BinaryAttribute::Type::Double:\n        return \"double\";\n    default:\n        assert(false); // bad attribute type\n        return \"unknown\";\n    };\n}\n\nPly::Ply() : vertexCount(0), vertexSize(0)\n{\n    ;\n}\n\nbool Ply::Parse(std::ifstream& plyFile)\n{\n    if (!ParseHeader(plyFile))\n    {\n        return false;\n    }\n\n    // read rest of file into data ptr\n    {\n        ZoneScopedNC(\"Ply::Parse() read data\", tracy::Color::Yellow);\n        AllocData(vertexCount);\n        plyFile.read((char*)data.get(), vertexSize * vertexCount);\n    }\n\n    return true;\n}\n\nvoid Ply::Dump(std::ofstream& plyFile) const\n{\n    DumpHeader(plyFile);\n    plyFile.write((char*)data.get(), vertexSize * vertexCount);\n}\n\nbool Ply::GetProperty(const std::string& key, BinaryAttribute& binaryAttributeOut) const\n{\n    auto iter = propertyMap.find(key);\n    if (iter != propertyMap.end())\n    {\n        binaryAttributeOut = iter->second;\n        return true;\n    }\n    return false;\n}\n\nvoid Ply::AddProperty(const std::string& key, BinaryAttribute::Type type)\n{\n    using PropInfoPair = std::pair<std::string, BinaryAttribute>;\n    BinaryAttribute attrib(type, vertexSize);\n    propertyMap.emplace(PropInfoPair(key, attrib));\n    vertexSize += attrib.size;\n}\n\nvoid Ply::AllocData(size_t numVertices)\n{\n    vertexCount = numVertices;\n    data.reset(new uint8_t[vertexSize * numVertices]);\n}\n\nvoid Ply::ForEachVertex(const VertexCallback& cb) const\n{\n    const uint8_t* ptr = data.get();\n    for (size_t i = 0; i < vertexCount; i++)\n    {\n        cb(ptr, vertexSize);\n        ptr += vertexSize;\n    }\n}\n\nvoid Ply::ForEachVertexMut(const VertexCallbackMut& cb)\n{\n    uint8_t* ptr = data.get();\n    for (size_t i = 0; i < vertexCount; i++)\n    {\n        cb(ptr, vertexSize);\n        ptr += vertexSize;\n    }\n}\n\nbool Ply::ParseHeader(std::ifstream& plyFile)\n{\n    ZoneScopedNC(\"Ply::ParseHeader\", tracy::Color::Green);\n\n    // validate start of header\n    std::string token1, token2, token3;\n\n    // check header starts with \"ply\".\n    if (!GetNextPlyLine(plyFile, token1))\n    {\n        Log::E(\"Unexpected error reading next line\\n\");\n        return false;\n    }\n    if (token1 != \"ply\")\n    {\n        Log::E(\"Invalid ply file\\n\");\n        return false;\n    }\n\n    // check format\n    if (!GetNextPlyLine(plyFile, token1))\n    {\n        Log::E(\"Unexpected error reading next line\\n\");\n        return false;\n    }\n    if (token1 != \"format binary_little_endian 1.0\" && token1 != \"format binary_big_endian 1.0\")\n    {\n        Log::E(\"Invalid ply file, expected format\\n\");\n        return false;\n    }\n    if (token1 != \"format binary_little_endian 1.0\")\n    {\n        Log::E(\"Unsupported ply file, only binary_little_endian supported\\n\");\n        return false;\n    }\n\n    // parse \"element vertex {number}\"\n    std::string line;\n    if (!GetNextPlyLine(plyFile, line))\n    {\n        Log::E(\"Unexpected error reading next line\\n\");\n        return false;\n    }\n    std::istringstream iss(line);\n    if (!((iss >> token1 >> token2 >> vertexCount) && (token1 == \"element\") && (token2 == \"vertex\")))\n    {\n        Log::E(\"Invalid ply file, expected \\\"element vertex {number}\\\"\\n\");\n        return false;\n    }\n\n    // TODO: support other \"element\" types faces, edges etc?\n    // at the moment I only care about ply files with vertex elements.\n\n    while (true)\n    {\n        if (!GetNextPlyLine(plyFile, line))\n        {\n            Log::E(\"unexpected error reading line\\n\");\n            return false;\n        }\n\n        if (line == \"end_header\")\n        {\n            break;\n        }\n\n        iss.str(line);\n        iss.clear();\n        iss >> token1 >> token2 >> token3;\n        if (token1 != \"property\")\n        {\n            Log::E(\"Invalid header, expected property\\n\");\n            return false;\n        }\n        if (token2 == \"char\" || token2 == \"int8\")\n        {\n            AddProperty(token3, BinaryAttribute::Type::Char);\n        }\n        else if (token2 == \"uchar\" || token2 == \"uint8\")\n        {\n            AddProperty(token3, BinaryAttribute::Type::UChar);\n        }\n        else if (token2 == \"short\" || token2 == \"int16\")\n        {\n            AddProperty(token3, BinaryAttribute::Type::Short);\n        }\n        else if (token2 == \"ushort\" || token2 == \"uint16\")\n        {\n            AddProperty(token3, BinaryAttribute::Type::UShort);\n        }\n        else if (token2 == \"int\" || token2 == \"int32\")\n        {\n            AddProperty(token3, BinaryAttribute::Type::Int);\n        }\n        else if (token2 == \"uint\" || token2 == \"uint32\")\n        {\n            AddProperty(token3, BinaryAttribute::Type::UInt);\n        }\n        else if (token2 == \"float\" || token2 == \"float32\")\n        {\n            AddProperty(token3, BinaryAttribute::Type::Float);\n        }\n        else if (token2 == \"double\" || token2 == \"float64\")\n        {\n            AddProperty(token3, BinaryAttribute::Type::Double);\n        }\n        else\n        {\n            Log::E(\"Unsupported type \\\"%s\\\" for property \\\"%s\\\"\\n\", token2.c_str(), token3.c_str());\n            return false;\n        }\n    }\n\n    return true;\n}\n\nvoid Ply::DumpHeader(std::ofstream& plyFile) const\n{\n    // ply files have unix line endings.\n    plyFile << \"ply\\n\";\n    plyFile << \"format binary_little_endian 1.0\\n\";\n    plyFile << \"element vertex \" << vertexCount << \"\\n\";\n\n    // sort properties by offset\n    using PropInfoPair = std::pair<std::string, BinaryAttribute>;\n    std::vector<PropInfoPair> propVec;\n    propVec.reserve(propertyMap.size());\n    for (auto& pair : propertyMap)\n    {\n        propVec.push_back(pair);\n    }\n    std::sort(propVec.begin(), propVec.end(), [](const PropInfoPair& a, const PropInfoPair& b)\n    {\n        return a.second.offset < b.second.offset;\n    });\n\n    for (auto& pair : propVec)\n    {\n        plyFile << \"property \" << BinaryAttributeTypeToString(pair.second.type) << \" \" << pair.first << \"\\n\";\n    }\n    plyFile << \"end_header\\n\";\n}\n"
  },
  {
    "path": "src/ply.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n\n#include <cassert>\n#include <cstdint>\n#include <functional>\n#include <memory>\n#include <string>\n#include <unordered_map>\n#include <vector>\n\n#include \"core/binaryattribute.h\"\n\nclass Ply\n{\npublic:\n    Ply();\n    bool Parse(std::ifstream& plyFile);\n    void Dump(std::ofstream& plyFile) const;\n\n    bool GetProperty(const std::string& key, BinaryAttribute& attributeOut) const;\n    void AddProperty(const std::string& key, BinaryAttribute::Type type);\n    void AllocData(size_t numVertices);\n\n    using VertexCallback = std::function<void(const void*, size_t)>;\n    void ForEachVertex(const VertexCallback& cb) const;\n\n    using VertexCallbackMut = std::function<void(void*, size_t)>;\n    void ForEachVertexMut(const VertexCallbackMut& cb);\n\n    size_t GetVertexCount() const { return vertexCount; }\n\nprotected:\n    bool ParseHeader(std::ifstream& plyFile);\n    void DumpHeader(std::ofstream& plyFile) const;\n\n    std::unordered_map<std::string, BinaryAttribute> propertyMap;\n    std::unique_ptr<uint8_t> data;\n    size_t vertexCount;\n    size_t vertexSize;\n};\n"
  },
  {
    "path": "src/pointcloud.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"pointcloud.h\"\n\n#include <cassert>\n#include <fstream>\n#include <iostream>\n#include <sstream>\n#include <string>\n\n#include \"core/log.h\"\n#include \"core/util.h\"\n#include \"ply.h\"\n\nstruct PointData\n{\n    PointData() noexcept {}\n    float position[4];\n    float color[4];\n};\n\nPointCloud::PointCloud(bool useLinearColorsIn) :\n    numPoints(0),\n    pointSize(0),\n    useLinearColors(useLinearColorsIn)\n{\n    ;\n}\n\nbool PointCloud::ImportPly(const std::string& plyFilename)\n{\n    std::ifstream plyFile(plyFilename, std::ios::binary);\n    if (!plyFile.is_open())\n    {\n        Log::E(\"failed to open \\\"%s\\\"\\n\", plyFilename.c_str());\n        return false;\n    }\n\n    Ply ply;\n    if (!ply.Parse(plyFile))\n    {\n        Log::E(\"Error parsing ply file \\\"%s\\\"\\n\", plyFilename.c_str());\n        return false;\n    }\n\n    struct\n    {\n        BinaryAttribute x, y, z;\n        BinaryAttribute red, green, blue;\n    } props;\n\n    if (!ply.GetProperty(\"x\", props.x) ||\n        !ply.GetProperty(\"y\", props.y) ||\n        !ply.GetProperty(\"z\", props.z))\n    {\n        Log::E(\"Error parsing ply file \\\"%s\\\", missing position property\\n\", plyFilename.c_str());\n    }\n\n    bool useDoubles = (props.x.type == BinaryAttribute::Type::Double &&\n                       props.y.type == BinaryAttribute::Type::Double &&\n                       props.z.type == BinaryAttribute::Type::Double);\n\n    if (!ply.GetProperty(\"red\", props.red) ||\n        !ply.GetProperty(\"green\", props.green) ||\n        !ply.GetProperty(\"blue\", props.blue))\n    {\n        Log::E(\"Error parsing ply file \\\"%s\\\", missing color property\\n\", plyFilename.c_str());\n    }\n\n    numPoints = ply.GetVertexCount();\n    pointSize = sizeof(PointData);\n    InitAttribs();\n    PointData* pd = new PointData[numPoints];\n    data.reset(pd);\n\n    if (useDoubles)\n    {\n        int i = 0;\n        ply.ForEachVertex([this, pd, &i, &props](const void* data, size_t size)\n        {\n            if (useLinearColors)\n            {\n                pd[i].position[0] = SRGBToLinear((float)props.x.Read<double>(data));\n                pd[i].position[1] = SRGBToLinear((float)props.y.Read<double>(data));\n                pd[i].position[2] = SRGBToLinear((float)props.z.Read<double>(data));\n            }\n            else\n            {\n                pd[i].position[0] = (float)props.x.Read<double>(data);\n                pd[i].position[1] = (float)props.y.Read<double>(data);\n                pd[i].position[2] = (float)props.z.Read<double>(data);\n            }\n            pd[i].position[3] = 1.0f;\n            pd[i].color[0] = (float)props.red.Read<uint8_t>(data) / 255.0f;\n            pd[i].color[1] = (float)props.green.Read<uint8_t>(data) / 255.0f;\n            pd[i].color[2] = (float)props.blue.Read<uint8_t>(data) / 255.0f;\n            pd[i].color[3] = 1.0f;\n            i++;\n        });\n    }\n    else\n    {\n        int i = 0;\n        ply.ForEachVertex([this, pd, &i, &props](const void* data, size_t size)\n        {\n            if (useLinearColors)\n            {\n                pd[i].position[0] = SRGBToLinear(props.x.Read<float>(data));\n                pd[i].position[1] = SRGBToLinear(props.y.Read<float>(data));\n                pd[i].position[2] = SRGBToLinear(props.z.Read<float>(data));\n            }\n            else\n            {\n                pd[i].position[0] = props.x.Read<float>(data);\n                pd[i].position[1] = props.y.Read<float>(data);\n                pd[i].position[2] = props.z.Read<float>(data);\n            }\n            pd[i].position[3] = 1.0f;\n            pd[i].color[0] = (float)props.red.Read<uint8_t>(data) / 255.0f;\n            pd[i].color[1] = (float)props.green.Read<uint8_t>(data) / 255.0f;\n            pd[i].color[2] = (float)props.blue.Read<uint8_t>(data) / 255.0f;\n            pd[i].color[3] = 1.0f;\n            i++;\n        });\n    }\n\n    return true;\n}\n\nbool PointCloud::ExportPly(const std::string& plyFilename) const\n{\n    std::ofstream plyFile(plyFilename, std::ios::binary);\n    if (!plyFile.is_open())\n    {\n        Log::E(\"failed to open %s\\n\", plyFilename.c_str());\n        return false;\n    }\n\n    Ply ply;\n    ply.AddProperty(\"x\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"y\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"z\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"nx\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"ny\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"nz\", BinaryAttribute::Type::Float);\n    ply.AddProperty(\"red\", BinaryAttribute::Type::UChar);\n    ply.AddProperty(\"green\", BinaryAttribute::Type::UChar);\n    ply.AddProperty(\"blue\", BinaryAttribute::Type::UChar);\n\n    struct\n    {\n        BinaryAttribute x, y, z;\n        BinaryAttribute nx, ny, nz;\n        BinaryAttribute red, green, blue;\n    } props;\n\n    ply.GetProperty(\"x\", props.x);\n    ply.GetProperty(\"y\", props.y);\n    ply.GetProperty(\"z\", props.z);\n    ply.GetProperty(\"nx\", props.nx);\n    ply.GetProperty(\"ny\", props.ny);\n    ply.GetProperty(\"nz\", props.nz);\n    ply.GetProperty(\"red\", props.red);\n    ply.GetProperty(\"green\", props.green);\n    ply.GetProperty(\"blue\", props.blue);\n\n    ply.AllocData(numPoints);\n\n    uint8_t* cloudData = (uint8_t*)data.get();\n    size_t runningSize = 0;\n    ply.ForEachVertexMut([this, &props, &cloudData, &runningSize](void* plyData, size_t size)\n    {\n        const float* position = positionAttrib.Get<float>(cloudData);\n        const float* color = colorAttrib.Get<float>(cloudData);\n\n        props.x.Write<float>(plyData, position[0]);\n        props.y.Write<float>(plyData, position[1]);\n        props.z.Write<float>(plyData, position[2]);\n        props.nx.Write<float>(plyData, 0.0f);\n        props.ny.Write<float>(plyData, 0.0f);\n        props.nz.Write<float>(plyData, 0.0f);\n        props.red.Write<uint8_t>(plyData, (uint8_t)(color[0] * 255.0f));\n        props.green.Write<uint8_t>(plyData, (uint8_t)(color[1] * 255.0f));\n        props.blue.Write<uint8_t>(plyData, (uint8_t)(color[2] * 255.0f));\n\n        cloudData += pointSize;\n        runningSize += pointSize;\n        assert(runningSize <= GetTotalSize());  // bad, we went outside of data ptr contents.\n    });\n\n    ply.Dump(plyFile);\n\n    return true;\n}\n\nvoid PointCloud::InitDebugCloud()\n{\n    const int NUM_POINTS = 5;\n\n    numPoints = NUM_POINTS * 3;\n    pointSize = sizeof(PointData);\n    InitAttribs();\n    PointData* pd = new PointData[numPoints];\n    data.reset(pd);\n\n    //\n    // make an debug pointVec, that contains three lines one for each axis.\n    //\n    const float AXIS_LENGTH = 1.0f;\n    const float DELTA = (AXIS_LENGTH / (float)NUM_POINTS);\n    // x axis\n    for (int i = 0; i < NUM_POINTS; i++)\n    {\n        PointData& p = pd[i];\n        p.position[0] = i * DELTA;\n        p.position[1] = 0.0f;\n        p.position[2] = 0.0f;\n        p.position[3] = 1.0f;\n        p.color[0] = 1.0f;\n        p.color[1] = 0.0f;\n        p.color[2] = 0.0f;\n        p.color[3] = 1.0f;\n    }\n    // y axis\n    for (int i = 0; i < NUM_POINTS; i++)\n    {\n        PointData& p = pd[i + NUM_POINTS];\n        p.position[0] = 0.0f;\n        p.position[1] = i * DELTA;\n        p.position[2] = 0.0f;\n        p.position[3] = 1.0f;\n        p.color[0] = 0.0f;\n        p.color[1] = 1.0f;\n        p.color[2] = 0.0f;\n        p.color[3] = 1.0f;\n    }\n    // z axis\n    for (int i = 0; i < NUM_POINTS; i++)\n    {\n        PointData& p = pd[(2 * NUM_POINTS) + i];\n        p.position[0] = 0.0f;\n        p.position[1] = 0.0f;\n        p.position[2] = i * DELTA;\n        p.position[3] = 1.0f;\n        p.color[0] = 0.0f;\n        p.color[1] = 0.0f;\n        p.color[2] = 1.0f;\n        p.color[3] = 1.0f;\n    }\n}\n\nvoid PointCloud::ForEachPosition(const ForEachPositionCallback& cb) const\n{\n    positionAttrib.ForEach<float>(GetRawDataPtr(), GetStride(), GetNumPoints(), cb);\n}\n\nvoid PointCloud::InitAttribs()\n{\n    positionAttrib = {BinaryAttribute::Type::Float, offsetof(PointData, position)};\n    colorAttrib = {BinaryAttribute::Type::Float, offsetof(PointData, color)};\n}\n"
  },
  {
    "path": "src/pointcloud.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <cstdint>\n#include <functional>\n#include <memory>\n#include <string>\n\n#include \"core/binaryattribute.h\"\n\nclass PointCloud\n{\npublic:\n    PointCloud(bool useLinearColorsIn);\n\n    bool ImportPly(const std::string& plyFilename);\n    bool ExportPly(const std::string& plyFilename) const;\n\n    void InitDebugCloud();\n\n    size_t GetNumPoints() const { return numPoints; }\n    size_t GetStride() const { return pointSize; }\n    size_t GetTotalSize() const { return GetNumPoints() * GetStride(); }\n    void* GetRawDataPtr() { return data.get(); }\n    const void* GetRawDataPtr() const { return data.get(); }\n\n    const BinaryAttribute& GetPositionAttrib() const { return positionAttrib; }\n    const BinaryAttribute& GetColorAttrib() const { return colorAttrib; }\n\n    using ForEachPositionCallback = std::function<void(const float*)>;\n    void ForEachPosition(const ForEachPositionCallback& cb) const;\n\nprotected:\n    void InitAttribs();\n\n    std::shared_ptr<void> data;\n\n    BinaryAttribute positionAttrib;\n    BinaryAttribute colorAttrib;\n\n    size_t numPoints;\n    size_t pointSize;\n    bool useLinearColors;\n};\n"
  },
  {
    "path": "src/pointrenderer.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"pointrenderer.h\"\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#else\n#include <GL/glew.h>\n#endif\n\n#include <glm/gtc/matrix_transform.hpp>\n\n#ifdef TRACY_ENABLE\n#include <tracy/Tracy.hpp>\n#else\n#define ZoneScoped\n#define ZoneScopedNC(NAME, COLOR)\n#endif\n\n#include \"core/image.h\"\n#include \"core/log.h\"\n#include \"core/texture.h\"\n#include \"core/util.h\"\n\n#include \"radix_sort.hpp\"\n\nstatic void SetupAttrib(int loc, const BinaryAttribute& attrib, int32_t numElems, size_t stride)\n{\n    assert(attrib.type == BinaryAttribute::Type::Float);\n    glVertexAttribPointer(loc, numElems, GL_FLOAT, GL_FALSE, (uint32_t)stride, (void*)attrib.offset);\n    glEnableVertexAttribArray(loc);\n}\n\nPointRenderer::PointRenderer()\n{\n}\n\nPointRenderer::~PointRenderer()\n{\n}\n\nbool PointRenderer::Init(std::shared_ptr<PointCloud> pointCloud, bool isFramebufferSRGBEnabledIn)\n{\n    GL_ERROR_CHECK(\"PointRenderer::Init() begin\");\n\n    isFramebufferSRGBEnabled = isFramebufferSRGBEnabledIn;\n\n    Image pointImg;\n    if (!pointImg.Load(\"texture/sphere.png\"))\n    {\n        Log::E(\"Error loading sphere.png\\n\");\n        return false;\n    }\n    pointImg.isSRGB = isFramebufferSRGBEnabled;\n\n    Texture::Params texParams = {FilterType::LinearMipmapLinear, FilterType::Linear, WrapType::ClampToEdge, WrapType::ClampToEdge};\n    pointTex = std::make_shared<Texture>(pointImg, texParams);\n\n    pointProg = std::make_shared<Program>();\n    if (!pointProg->LoadVertGeomFrag(\"shader/point_vert.glsl\", \"shader/point_geom.glsl\", \"shader/point_frag.glsl\"))\n    {\n        Log::E(\"Error loading point shaders!\\n\");\n        return false;\n    }\n\n    preSortProg = std::make_shared<Program>();\n    if (!preSortProg->LoadCompute(\"shader/presort_compute.glsl\"))\n    {\n        Log::E(\"Error loading point pre-sort compute shader!\\n\");\n        return false;\n    }\n\n    const size_t numPoints = pointCloud->GetNumPoints();\n\n    // build posVec\n    posVec.reserve(numPoints);\n    pointCloud->ForEachPosition([this](const float* pos)\n    {\n        posVec.emplace_back(glm::vec4(pos[0], pos[1], pos[2], pos[3]));\n    });\n\n    BuildVertexArrayObject(pointCloud);\n\n    depthVec.resize(numPoints);\n    keyBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, depthVec, GL_DYNAMIC_STORAGE_BIT);\n    valBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT);\n    posBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, posVec);\n    sorter = std::make_shared<rgc::radix_sort::sorter>(numPoints);\n\n    atomicCounterVec.resize(1, 0);\n    atomicCounterBuffer = std::make_shared<BufferObject>(GL_ATOMIC_COUNTER_BUFFER, atomicCounterVec, GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT);\n\n    GL_ERROR_CHECK(\"PointRenderer::Init() end\");\n\n    return true;\n}\n\nvoid PointRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                           const glm::vec4& viewport, const glm::vec2& nearFar)\n{\n    ZoneScoped;\n\n    GL_ERROR_CHECK(\"PointRenderer::Render() begin\");\n\n    const size_t numPoints = posVec.size();\n    glm::mat4 modelViewMat = glm::inverse(cameraMat);\n\n    const uint32_t MAX_DEPTH = std::numeric_limits<uint32_t>::max();\n\n    {\n        ZoneScopedNC(\"pre-sort\", tracy::Color::Red4);\n\n        preSortProg->Bind();\n        preSortProg->SetUniform(\"modelViewProj\", projMat * modelViewMat);\n        preSortProg->SetUniform(\"nearFar\", nearFar);\n        preSortProg->SetUniform(\"keyMax\", MAX_DEPTH);\n\n        glm::mat4 modelViewProjMat = projMat * modelViewMat;\n\n        // reset counter back to 0\n        atomicCounterVec[0] = 0;\n        atomicCounterBuffer->Update(atomicCounterVec);\n\n        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, posBuffer->GetObj());\n        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, keyBuffer->GetObj());\n        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, valBuffer->GetObj());\n        glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 4, atomicCounterBuffer->GetObj());\n\n        const int LOCAL_SIZE = 256;\n        glDispatchCompute(((GLuint)numPoints + (LOCAL_SIZE - 1)) / LOCAL_SIZE, 1, 1); // Assuming LOCAL_SIZE threads per group\n        glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT);\n\n        GL_ERROR_CHECK(\"PointRenderer::Render() pre-sort\");\n    }\n\n    uint32_t sortCount = 0;\n    {\n        ZoneScopedNC(\"get-count\", tracy::Color::Green);\n\n        atomicCounterBuffer->Read(atomicCounterVec);\n        sortCount = atomicCounterVec[0];\n\n        assert(sortCount <= (uint32_t)numPoints);\n\n        GL_ERROR_CHECK(\"PointRenderer::Render() get-count\");\n    }\n\n    {\n        ZoneScopedNC(\"sort\", tracy::Color::Red4);\n\n        sorter->sort(keyBuffer->GetObj(), valBuffer->GetObj(), sortCount);\n\n        GL_ERROR_CHECK(\"PointRenderer::Render() sort\");\n    }\n\n    {\n        ZoneScopedNC(\"copy-sorted\", tracy::Color::DarkGreen);\n\n        glBindBuffer(GL_COPY_READ_BUFFER, valBuffer->GetObj());\n        glBindBuffer(GL_COPY_WRITE_BUFFER, pointVao->GetElementBuffer()->GetObj());\n        glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sortCount * sizeof(uint32_t));\n\n        GL_ERROR_CHECK(\"PointRenderer::Render() copy-sorted\");\n    }\n\n    {\n        ZoneScopedNC(\"draw\", tracy::Color::Red4);\n\n        float width = viewport.z;\n        float height = viewport.w;\n        float aspectRatio = width / height;\n\n        pointProg->Bind();\n        pointProg->SetUniform(\"modelViewMat\", modelViewMat);\n        pointProg->SetUniform(\"projMat\", projMat);\n        pointProg->SetUniform(\"pointSize\", 0.02f);  // in ndc space?!?\n        pointProg->SetUniform(\"invAspectRatio\", 1.0f / aspectRatio);\n\n        // use texture unit 0 for colorTexture\n        glActiveTexture(GL_TEXTURE0);\n        glBindTexture(GL_TEXTURE_2D, pointTex->texture);\n        pointProg->SetUniform(\"colorTex\", 0);\n\n        pointVao->Bind();\n        glDrawElements(GL_POINTS, sortCount, GL_UNSIGNED_INT, nullptr);\n        pointVao->Unbind();\n\n        GL_ERROR_CHECK(\"PointRenderer::Render() draw\");\n    }\n}\n\nvoid PointRenderer::BuildVertexArrayObject(std::shared_ptr<PointCloud> pointCloud)\n{\n    pointVao = std::make_shared<VertexArrayObject>();\n\n    const size_t numPoints = pointCloud->GetNumPoints();\n\n    // allocate large buffer to hold interleaved vertex data\n    pointDataBuffer = std::make_shared<BufferObject>(GL_ARRAY_BUFFER, pointCloud->GetRawDataPtr(),\n                                                     pointCloud->GetTotalSize(), 0);\n\n    // build element array\n    indexVec.reserve(numPoints);\n    assert(numPoints <= std::numeric_limits<uint32_t>::max());\n    for (uint32_t i = 0; i < (uint32_t)numPoints; i++)\n    {\n        indexVec.push_back(i);\n    }\n    auto indexBuffer = std::make_shared<BufferObject>(GL_ELEMENT_ARRAY_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT);\n\n    pointVao->Bind();\n    pointDataBuffer->Bind();\n\n    SetupAttrib(pointProg->GetAttribLoc(\"position\"), pointCloud->GetPositionAttrib(), 4, pointCloud->GetStride());\n    SetupAttrib(pointProg->GetAttribLoc(\"color\"), pointCloud->GetColorAttrib(), 4, pointCloud->GetStride());\n\n    pointVao->SetElementBuffer(indexBuffer);\n    pointDataBuffer->Unbind();\n}\n"
  },
  {
    "path": "src/pointrenderer.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <memory>\n#include <stdint.h>\n#include <vector>\n\n#include \"core/program.h\"\n#include \"core/texture.h\"\n#include \"core/vertexbuffer.h\"\n\n#include \"pointcloud.h\"\n\nnamespace rgc::radix_sort\n{\n    struct sorter;\n}\n\nclass PointRenderer\n{\npublic:\n    PointRenderer();\n    ~PointRenderer();\n\n    bool Init(std::shared_ptr<PointCloud> pointCloud, bool isFramebufferSRGBEnabledIn);\n\n    // viewport = (x, y, width, height)\n    void Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                const glm::vec4& viewport, const glm::vec2& nearFar);\nprotected:\n    void BuildVertexArrayObject(std::shared_ptr<PointCloud> pointCloud);\n\n    std::shared_ptr<Texture> pointTex;\n    std::shared_ptr<Program> pointProg;\n    std::shared_ptr<Program> preSortProg;\n    std::shared_ptr<VertexArrayObject> pointVao;\n\n    std::shared_ptr<BufferObject> pointDataBuffer;\n\n    std::vector<uint32_t> indexVec;\n    std::vector<uint32_t> depthVec;\n    std::vector<glm::vec4> posVec;\n    std::vector<uint32_t> atomicCounterVec;\n\n    std::shared_ptr<BufferObject> keyBuffer;\n    std::shared_ptr<BufferObject> valBuffer;\n    std::shared_ptr<BufferObject> posBuffer;\n    std::shared_ptr<BufferObject> atomicCounterBuffer;\n\n    std::shared_ptr<rgc::radix_sort::sorter> sorter;\n    bool isFramebufferSRGBEnabled;\n};\n"
  },
  {
    "path": "src/radix_sort.hpp",
    "content": "/*\n    MIT License\n\n    Copyright (c) 2021 Lorenzo Rutayisire\n\n    Permission is hereby granted, free of charge, to any person obtaining a copy\n    of this software and associated documentation files (the \"Software\"), to deal\n    in the Software without restriction, including without limitation the rights\n    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n    copies of the Software, and to permit persons to whom the Software is\n    furnished to do so, subject to the following conditions:\n\n    The above copyright notice and this permission notice shall be included in all\n    copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n    SOFTWARE.\n\n\thttps://github.com/loryruta/gpu-radix-sort\n*/\n#pragma once\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#include <GLES3/gl32.h>\n#else\n#include <GL/glew.h> // Implement your own OpenGL functions loading library.\n#endif\n\n#include <array>\n#include <fstream>\n#include <filesystem>\n#include <iostream>\n\n#ifdef __ANDROID__\n// glBufferStorage is not present in OpenGLES 3.2\nstatic void glBufferStorage(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags)\n{\n\tGLenum usage = 0;\n\tif (flags & GL_DYNAMIC_STORAGE_BIT)\n\t{\n\t\tif (flags & GL_MAP_READ_BIT)\n\t\t{\n\t\t\tusage = GL_DYNAMIC_READ;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tusage = GL_DYNAMIC_DRAW;\n\t\t}\n\t}\n\telse\n\t{\n\t\tif (flags & GL_MAP_READ_BIT)\n\t\t{\n\t\t\tusage = GL_STATIC_READ;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tusage = GL_STATIC_DRAW;\n\t\t}\n\t}\n\tglBufferData(target, size, data, usage);\n}\n\nstatic void ZeroBuffer(GLenum target, size_t size)\n{\n\tstatic std::vector<uint8_t> zeroVec;\n\tzeroVec.resize(size, 0);\n\tglBufferSubData(target, 0, size, (void*)zeroVec.data());\n}\n#endif\n\n#define RGC_RADIX_SORT_THREADS_PER_BLOCK 64\n#define RGC_RADIX_SORT_ITEMS_PER_THREAD  4\n#define RGC_RADIX_SORT_BITSET_NUM   4\n#define RGC_RADIX_SORT_BITSET_COUNT ((sizeof(GLuint) * 8) / RGC_RADIX_SORT_BITSET_NUM)\n#define RGC_RADIX_SORT_BITSET_SIZE  GLuint(exp2(RGC_RADIX_SORT_BITSET_NUM))\n\n#ifdef RGC_RADIX_SORT_DEBUG\n\t#include \"renderdoc.hpp\"\n\t#define RGC_RADIX_SORT_RENDERDOC_WATCH(capture, f) rgc::renderdoc::watch(capture, f)\n#else\n\t#define RGC_RADIX_SORT_RENDERDOC_WATCH(capture, f) f()\n#endif\n\ninline void __rgc_shader_injector_load_src(GLuint shader, char const* src)\n{\n#ifdef __ANDROID__\n\tstd::string version = \"#version 320 es\\n\";\n#else\n\tstd::string version = \"#version 460\\n\";\n#endif\n\tstd::string source = version + src;\n\tconst GLchar* sourcePtr = source.c_str();\n\tglShaderSource(shader, 1, (const GLchar**)&sourcePtr, nullptr);\n}\n\ninline void __rgc_shader_injector_load_src_from_file(GLuint shader, std::filesystem::path const& filename)\n{\n\tstd::ifstream file(filename);\n\tif (!file.is_open())\n\t{\n\t\tstd::cerr << \"Failed to open file at: \" << filename << std::endl;\n\t\tthrow std::invalid_argument(\"Failed to open text file.\");\n\t}\n\n\tstd::string src((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());\n\t__rgc_shader_injector_load_src(shader, src.c_str());\n}\n\n#define RGC_SHADER_INJECTOR_INJECTION_POINT\n#define RGC_SHADER_INJECTOR_LOAD_SRC(shader, path) __rgc_shader_injector_load_src_from_file(shader, path)\n\ninline const char* __rgc_shader_injector_shader_src_6213812085af7087c07246dabc62da60 = \"\\n#define THREAD_IDX        gl_LocalInvocationIndex\\n#define THREADS_NUM       64\\n#define THREAD_BLOCK_IDX  (gl_WorkGroupID.x + gl_NumWorkGroups.x * (gl_WorkGroupID.y + gl_NumWorkGroups.z * gl_WorkGroupID.z))\\n#define THREAD_BLOCKS_NUM (gl_NumWorkGroups.x * gl_NumWorkGroups.y * gl_NumWorkGroups.z)\\n#define ITEMS_NUM         4u\\n\\n#define BITSET_NUM        4u\\n#define BITSET_SIZE       16u\\n\\nlayout(local_size_x = THREADS_NUM, local_size_y = 1, local_size_z = 1) in;\\n\\nlayout(std430, binding = 0) buffer ssbo_key           { uint b_key_buf[];  };\\nlayout(std430, binding = 1) buffer ssbo_count_buf     { uint b_count_buf[]; }; // [THREAD_BLOCKS_NUM * BITSET_SIZE]\\nlayout(std430, binding = 2) buffer ssbo_tot_count_buf { uint b_tot_count_buf[BITSET_SIZE]; };\\n\\nuniform uint u_arr_len;\\nuniform uint u_bitset_idx;\\n\\nuint to_partition_radixes_offsets_idx(uint radix, uint thread_block_idx)\\n{\\n    uint pow_of_2_thread_blocks_num = uint(exp2(ceil(log2(float(THREAD_BLOCKS_NUM)))));\\n    return radix * pow_of_2_thread_blocks_num + thread_block_idx;\\n}\\n\\nuint to_loc_idx(uint item_idx, uint thread_idx)\\n{\\n    return (thread_idx * ITEMS_NUM + item_idx);\\n}\\n\\nuint to_key_idx(uint item_idx, uint thread_idx, uint thread_block_idx)\\n{\\n    return (thread_block_idx * ITEMS_NUM * uint(THREADS_NUM)) + (thread_idx * ITEMS_NUM) + item_idx;\\n}\\n\\nvoid main()\\n{\\n    for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n    {\\n        uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n        if (key_idx >= u_arr_len) {\\n            continue;\\n        }\\n\\n        uint bitset_mask = (BITSET_SIZE - 1u) << (BITSET_NUM * u_bitset_idx);\\n        uint rad = (b_key_buf[key_idx] & bitset_mask) >> (BITSET_NUM * u_bitset_idx);\\n\\n        atomicAdd(b_count_buf[to_partition_radixes_offsets_idx(rad, THREAD_BLOCK_IDX)], 1u);\\n        atomicAdd(b_tot_count_buf[rad], 1u);\\n    }\\n}\\n\";\ninline const char* __rgc_shader_injector_shader_src_9992419d7253eb1e5310935c952c8eff = \"\\n#define THREAD_IDX        gl_LocalInvocationIndex\\n#define THREADS_NUM       64\\n#define THREAD_BLOCK_IDX  (gl_WorkGroupID.x + gl_NumWorkGroups.x * (gl_WorkGroupID.y + gl_NumWorkGroups.z * gl_WorkGroupID.z))\\n#define ITEMS_NUM         4u\\n#define BITSET_NUM        4u\\n#define BITSET_SIZE       16u\\n\\n#define OP_UPSWEEP    0u\\n#define OP_CLEAR_LAST 1u\\n#define OP_DOWNSWEEP  2u\\n\\nlayout(local_size_x = THREADS_NUM, local_size_y = 1, local_size_z = 1) in;\\n\\nlayout(std430, binding = 0) buffer ssbo_local_offsets_buf { uint b_local_offsets_buf[]; }; // b_count_buf[THREAD_BLOCKS_NUM * BITSET_SIZE]\\n\\nuniform uint u_arr_len; // Already guaranteed to be a power of 2\\nuniform uint u_depth;\\nuniform uint u_op;\\n\\nuint to_partition_radixes_offsets_idx(uint radix, uint thread_block_idx)\\n{\\n    return radix * u_arr_len + thread_block_idx;\\n}\\n\\nuint to_loc_idx(uint item_idx, uint thread_idx)\\n{\\n    return (thread_idx * ITEMS_NUM + item_idx);\\n}\\n\\nuint to_key_idx(uint item_idx, uint thread_idx, uint thread_block_idx)\\n{\\n    return (thread_block_idx * ITEMS_NUM * uint(THREADS_NUM)) + (thread_idx * ITEMS_NUM) + item_idx;\\n}\\n\\nvoid main()\\n{\\n    if (uint(fract(log2(float(u_arr_len)))) != 0u) {\\n        return; // ERROR: The u_arr_len must be a power of 2 otherwise the Blelloch scan won't work!\\n    }\\n\\n    // ------------------------------------------------------------------------------------------------\\n    // Blelloch scan\\n    // ------------------------------------------------------------------------------------------------\\n\\n    uint step = uint(exp2(float(u_depth)));\\n\\n    if (u_op == OP_UPSWEEP)\\n    {\\n        // Reduce (upsweep)\\n        for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n        {\\n            uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n            if (key_idx % (step * 2u) == 0u)\\n            {\\n                uint from_idx = key_idx + (step - 1u);\\n                uint to_idx = from_idx + step;\\n\\n                if (to_idx < u_arr_len)\\n                {\\n                    for (uint rad = 0u; rad < BITSET_SIZE; rad++)\\n                    {\\n                        uint from_rad_idx = to_partition_radixes_offsets_idx(rad, from_idx);\\n                        uint to_rad_idx = to_partition_radixes_offsets_idx(rad, to_idx);\\n\\n                        b_local_offsets_buf[to_rad_idx] = b_local_offsets_buf[from_rad_idx] + b_local_offsets_buf[to_rad_idx];\\n                    }\\n                }\\n            }\\n        }\\n    }\\n    else if (u_op == OP_DOWNSWEEP)\\n    {\\n        // Downsweep\\n        for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n        {\\n            uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n            if (key_idx % (step * 2u) == 0u)\\n            {\\n                uint from_idx = key_idx + (step - 1u);\\n                uint to_idx = from_idx + step;\\n\\n                if (to_idx < u_arr_len)\\n                {\\n                    for (uint rad = 0u; rad < BITSET_SIZE; rad++)\\n                    {\\n                        uint from_rad_idx = to_partition_radixes_offsets_idx(rad, from_idx);\\n                        uint to_rad_idx = to_partition_radixes_offsets_idx(rad, to_idx);\\n\\n                        uint r = b_local_offsets_buf[to_rad_idx];\\n                        b_local_offsets_buf[to_rad_idx] = b_local_offsets_buf[from_rad_idx] + b_local_offsets_buf[to_rad_idx];\\n                        b_local_offsets_buf[from_rad_idx] = r;\\n                    }\\n                }\\n            }\\n        }\\n    }\\n    else// if (u_op == OP_CLEAR_LAST)\\n    {\\n        for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n        {\\n            uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n            if (key_idx == (u_arr_len - 1u))\\n            {\\n                for (uint rad = 0u; rad < BITSET_SIZE; rad++)\\n                {\\n                    uint idx = to_partition_radixes_offsets_idx(rad, key_idx);\\n                    b_local_offsets_buf[idx] = 0u;\\n                }\\n            }\\n        }\\n    }\\n}\\n\";\ninline const char* __rgc_shader_injector_shader_src_8c254d92f3f4855a006b5a5c319997b2 = \"\\n#define THREAD_IDX        gl_LocalInvocationIndex\\n#define THREADS_NUM       64\\n#define THREAD_BLOCK_IDX  (gl_WorkGroupID.x + gl_NumWorkGroups.x * (gl_WorkGroupID.y + gl_NumWorkGroups.z * gl_WorkGroupID.z))\\n#define THREAD_BLOCKS_NUM (gl_NumWorkGroups.x * gl_NumWorkGroups.y * gl_NumWorkGroups.z)\\n#define ITEMS_NUM         4u\\n#define BITSET_NUM        4u\\n#define BITSET_SIZE       16u\\n\\n#define UINT32_MAX uint(-1)\\n\\nlayout(local_size_x = THREADS_NUM, local_size_y = 1, local_size_z = 1) in;\\n\\nlayout(std430, binding = 0) restrict readonly buffer in_keys_buf\\n{\\n    uint b_in_keys[];\\n};\\n\\nlayout(std430, binding = 1) restrict writeonly buffer out_keys_buf\\n{\\n    uint b_out_keys[];\\n};\\n\\nlayout(std430, binding = 2) restrict readonly buffer in_values_buf\\n{\\n    uint b_in_values[];\\n};\\n\\nlayout(std430, binding = 3) restrict writeonly buffer out_values_buf\\n{\\n    uint b_out_values[];\\n};\\n\\nlayout(std430, binding = 4) restrict readonly buffer local_offsets_buf\\n{\\n    uint b_local_offsets_buf[];\\n};\\n\\nlayout(std430, binding = 5) restrict readonly buffer global_counts_buf\\n{\\n    uint b_glob_counts_buf[BITSET_SIZE];\\n};\\n\\nuniform uint u_arr_len;\\nuniform uint u_bitset_idx;\\nuniform uint u_write_values;\\n\\nshared uint s_prefix_sum[BITSET_SIZE][uint(THREADS_NUM) * ITEMS_NUM];\\nshared uint s_key_buf[uint(THREADS_NUM) * ITEMS_NUM][2];\\nshared uint s_sorted_indices[uint(THREADS_NUM) * ITEMS_NUM][2];\\nshared uint s_count[BITSET_SIZE];\\n\\nuint to_partition_radixes_offsets_idx(uint radix, uint thread_block_idx)\\n{\\n    uint pow_of_2_thread_blocks_num = uint(exp2(ceil(log2(float(THREAD_BLOCKS_NUM)))));\\n    return radix * pow_of_2_thread_blocks_num + thread_block_idx;\\n}\\n\\nuint to_loc_idx(uint item_idx, uint thread_idx)\\n{\\n    return (thread_idx * ITEMS_NUM + item_idx);\\n}\\n\\nuint to_key_idx(uint item_idx, uint thread_idx, uint thread_block_idx)\\n{\\n    return (thread_block_idx * ITEMS_NUM * uint(THREADS_NUM)) + (thread_idx * ITEMS_NUM) + item_idx;\\n}\\n\\nvoid main()\\n{\\n    // ------------------------------------------------------------------------------------------------\\n    // Global offsets\\n    // ------------------------------------------------------------------------------------------------\\n\\n    // Runs an exclusive scan on the global counts to get the starting offset for each radix.\\n    // The offsets are global for the whole input array.\\n\\n    uint glob_off_buf[BITSET_SIZE];\\n\\n    // Exclusive scan\\n    for (uint sum = 0u, i = 0u; i < BITSET_SIZE; i++)\\n    {\\n        glob_off_buf[i] = sum;\\n        sum += b_glob_counts_buf[i];\\n    }\\n\\n    // ------------------------------------------------------------------------------------------------\\n    // Radix sort on partition\\n    // ------------------------------------------------------------------------------------------------\\n\\n    // NOTE:\\n    // The last partition potentially can have keys that aren't part of the original array.\\n    // These keys during the radix sort are set to `UINT32_MAX` (or 0xFFFFFFFF), this way, after the array is sorted\\n    // they'll always be in the last position and won't be confused with the original keys of the input array.\\n\\n    for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n    {\\n        uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n        uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n\\n        s_key_buf[loc_idx][0] = key_idx < u_arr_len ? b_in_keys[key_idx] : UINT32_MAX; // If the key_idx is outside of the input array then use UINT32_MAX.\\n        s_key_buf[loc_idx][1] = UINT32_MAX;\\n\\n        s_sorted_indices[loc_idx][0] = loc_idx;\\n        s_sorted_indices[loc_idx][1] = UINT32_MAX;\\n    }\\n\\n    barrier();\\n\\n    // The offsets of the radixes within the partition. The values of this array will be written every radix-sort iteration.\\n    uint in_partition_group_off[BITSET_SIZE];\\n\\n    uint bitset_idx;\\n    for (bitset_idx = 0u; bitset_idx <= u_bitset_idx; bitset_idx++)\\n    {\\n        uint bitset_mask = (BITSET_SIZE - 1u) << (BITSET_NUM * bitset_idx);\\n\\n        // Init\\n        for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n        {\\n            for (uint bitset_val = 0u; bitset_val < BITSET_SIZE; bitset_val++)\\n            {\\n                uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n                s_prefix_sum[bitset_val][loc_idx] = 0u;\\n            }\\n        }\\n\\n        barrier();\\n\\n        // ------------------------------------------------------------------------------------------------\\n        // Predicate test\\n        // ------------------------------------------------------------------------------------------------\\n\\n        // For each key of the current partition sets a 1 whether the radix corresponds to the radix of the current key.\\n        // Otherwise the default value is initialized to 0.\\n\\n        for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n        {\\n            uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n            uint k = s_key_buf[loc_idx][bitset_idx % 2u];\\n            uint radix = (k & bitset_mask) >> (BITSET_NUM * bitset_idx);\\n            s_prefix_sum[radix][loc_idx] = 1u;\\n        }\\n\\n        barrier();\\n\\n        // ------------------------------------------------------------------------------------------------\\n        // Exclusive sum\\n        // ------------------------------------------------------------------------------------------------\\n\\n        // THREADS_NUM * ITEMS_NUM are guaranteed to be a power of two, otherwise it won't work!\\n\\n        // An exclusive sum is run on the predicates, this way, for each location in the partition\\n        // we know the offset the element has to go, relative to its radix.\\n\\n        // Up-sweep\\n        for (uint d = 0u; d < uint(log2(float(uint(THREADS_NUM) * ITEMS_NUM))); d++)\\n        {\\n            for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n            {\\n                uint step = uint(exp2(float(d)));\\n                uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n\\n                if (loc_idx % (step * 2u) == 0u)\\n                {\\n                    uint from_idx = loc_idx + (step - 1u);\\n                    uint to_idx = from_idx + step;\\n\\n                    if (to_idx < uint(THREADS_NUM) * ITEMS_NUM)\\n                    {\\n                        for (uint bitset_val = 0u; bitset_val < BITSET_SIZE; bitset_val++)\\n                        {\\n                            s_prefix_sum[bitset_val][to_idx] = s_prefix_sum[bitset_val][from_idx] + s_prefix_sum[bitset_val][to_idx];\\n                        }\\n                    }\\n                }\\n            }\\n\\n            barrier();\\n        }\\n\\n        // Clear last\\n        if (THREAD_IDX == 0u)\\n        {\\n            for (uint bitset_val = 0u; bitset_val < BITSET_SIZE; bitset_val++)\\n            {\\n                s_prefix_sum[bitset_val][(uint(THREADS_NUM) * ITEMS_NUM) - 1u] = 0u;\\n            }\\n        }\\n\\n        barrier();\\n\\n        // Down-sweep\\n        for (int d = int(log2(float(uint(THREADS_NUM) * ITEMS_NUM))) - 1; d >= 0; d--)\\n        {\\n            for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n            {\\n                uint step = uint(exp2(float(d)));\\n                uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n\\n                if (loc_idx % (step * 2u) == 0u)\\n                {\\n                    uint from_idx = loc_idx + (step - 1u);\\n                    uint to_idx = from_idx + step;\\n\\n                    if (to_idx < uint(THREADS_NUM) * ITEMS_NUM)\\n                    {\\n                        for (uint bitset_val = 0u; bitset_val < BITSET_SIZE; bitset_val++)\\n                        {\\n                            uint r = s_prefix_sum[bitset_val][to_idx];\\n                            s_prefix_sum[bitset_val][to_idx] = r + s_prefix_sum[bitset_val][from_idx];\\n                            s_prefix_sum[bitset_val][from_idx] = r;\\n                        }\\n                    }\\n                }\\n            }\\n\\n            barrier();\\n        }\\n\\n        // ------------------------------------------------------------------------------------------------\\n        // Shuffling\\n        // ------------------------------------------------------------------------------------------------\\n\\n        uint last_loc_idx;\\n        if (THREAD_BLOCK_IDX == (THREAD_BLOCKS_NUM - 1u)) { // The last partition may be larger than the original size of the array, so the last position is before its end.\\n            last_loc_idx = u_arr_len - (THREAD_BLOCKS_NUM - 1u) * (uint(THREADS_NUM) * ITEMS_NUM) - 1u;\\n        } else {\\n            last_loc_idx = (uint(THREADS_NUM) * ITEMS_NUM) - 1u;\\n        }\\n\\n        // Now for every radix we need to know its 'global' offset within the partition (we only know relative offsets per location within the same radix).\\n        // So we just issue an exclusive scan (again) on the last element offset of every radix.\\n\\n        for (uint sum = 0u, i = 0u; i < BITSET_SIZE; i++) // Small prefix-sum to calculate group offsets.\\n        {\\n            in_partition_group_off[i] = sum;\\n\\n            // Since we need the count of every bit-group, if the last element is the bitset value itself, we need to count it.\\n            // Otherwise it will be already counted.\\n            bool is_last = ((s_key_buf[last_loc_idx][bitset_idx % 2u] & bitset_mask) >> (BITSET_NUM * bitset_idx)) == i;\\n            sum += s_prefix_sum[i][last_loc_idx] + (is_last ? 1u : 0u);\\n        }\\n\\n        for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n        {\\n            uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n            uint k = s_key_buf[loc_idx][bitset_idx % 2u];\\n            uint radix = (k & bitset_mask) >> (BITSET_NUM * bitset_idx);\\n\\n            // The destination address is calculated as the global offset of the radix plus the\\n            // local offset that depends by the location of the element.\\n            uint dest_addr = in_partition_group_off[radix] + s_prefix_sum[radix][loc_idx];\\n            s_key_buf[dest_addr][(bitset_idx + 1u) % 2u] = k; // Double-buffering is used every cycle the read and write buffer are swapped.\\n            s_sorted_indices[dest_addr][(bitset_idx + 1u) % 2u] = s_sorted_indices[loc_idx][bitset_idx % 2u];\\n        }\\n\\n        barrier();\\n    }\\n\\n    // TODO (REMOVE) WRITE s_prefix_sum\\n    /*\\n    for (uint item_idx = 0; item_idx < ITEMS_NUM; item_idx++)\\n    {\\n        uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n        if (item_idx < u_arr_len)\\n        {\\n            uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n            b_out_keys[key_idx] = s_prefix_sum[1][loc_idx];\\n        }\\n    }\\n\\n    for (uint item_idx = 0; item_idx < ITEMS_NUM; item_idx++)\\n    {\\n        uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n        if (key_idx < u_arr_len)\\n        {\\n            uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n            b_out_keys[key_idx] = s_key_buf[loc_idx][bitset_idx % 2];\\n        }\\n    }\\n    */\\n\\n    // ------------------------------------------------------------------------------------------------\\n    // Scattered writes to sorted partitions\\n    // ------------------------------------------------------------------------------------------------\\n\\n    uint bitset_mask = (BITSET_SIZE - 1u) << (BITSET_NUM * u_bitset_idx);\\n\\n    /*\\n    if (THREAD_IDX == 0)\\n    {\\n        for (uint i = 0; i < BITSET_SIZE; i++) {\\n            s_count[i] = 0;\\n        }\\n    }\\n\\n    barrier();\\n\\n    for (uint item_idx = 0; item_idx < ITEMS_NUM; item_idx++)\\n    {\\n        uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n        if (key_idx < u_arr_len)\\n        {\\n            uint k = s_key_buf[to_loc_idx(item_idx, THREAD_IDX)][bitset_idx % 2];\\n            uint rad = (k & bitset_mask) >> (BITSET_NUM * u_bitset_idx);\\n\\n            atomicAdd(s_count[rad], 1);\\n        }\\n    }\\n\\n    barrier();\\n\\n    for (uint item_idx = 0; item_idx < ITEMS_NUM; item_idx++)\\n    {\\n        uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n        if (key_idx < u_arr_len)\\n        {\\n            uint k = s_key_buf[to_loc_idx(item_idx, THREAD_IDX)][bitset_idx % 2];\\n            uint rad = (k & bitset_mask) >> (BITSET_NUM * u_bitset_idx);\\n\\n            b_out_keys[key_idx] = s_count[rad];\\n        }\\n    }\\n\\n    barrier();\\n    */\\n\\n    for (uint item_idx = 0u; item_idx < ITEMS_NUM; item_idx++)\\n    {\\n        uint key_idx = to_key_idx(item_idx, THREAD_IDX, THREAD_BLOCK_IDX);\\n        if (key_idx < u_arr_len)\\n        {\\n            uint loc_idx = to_loc_idx(item_idx, THREAD_IDX);\\n            uint k = s_key_buf[loc_idx][bitset_idx % 2u];\\n            uint rad = (k & bitset_mask) >> (BITSET_NUM * u_bitset_idx);\\n\\n            uint glob_off = glob_off_buf[rad];\\n            uint local_off = b_local_offsets_buf[to_partition_radixes_offsets_idx(rad, THREAD_BLOCK_IDX)];\\n\\n            uint dest_idx = glob_off + local_off + (loc_idx - in_partition_group_off[rad]);\\n\\n            b_out_keys[dest_idx] = k;\\n            if (u_write_values != 0u)\\n            {\\n                b_out_values[dest_idx] = b_in_values[THREAD_BLOCK_IDX * (uint(THREADS_NUM) * ITEMS_NUM) + s_sorted_indices[loc_idx][bitset_idx % 2u]];\\n            }\\n        }\\n    }\\n}\\n\";\n\n\nnamespace rgc::radix_sort\n{\n\tinline const GLuint k_zero = 0;\n\n\tstruct shader\n\t{\n\t\tGLuint m_name;\n\n\t\tshader(GLenum type)\n\t\t{\n\t\t\tm_name = glCreateShader(type);\n\t\t}\n\n\t\t~shader()\n\t\t{\n\t\t\tglDeleteShader(m_name);\n\t\t}\n\n\t\tvoid src_from_txt(char const* txt) const\n\t\t{\n\t\t\tglShaderSource(m_name, 1, &txt, nullptr);\n\t\t}\n\n\t\tvoid src_from_txt_file(std::filesystem::path const& filename) const\n\t\t{\n\t\t\tstd::ifstream file(filename);\n\t\t\tif (!file.is_open()) {\n\t\t\t\tthrow std::invalid_argument(\"Failed to open text file.\");\n\t\t\t}\n\n\t\t\tstd::string src((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());\n\t\t\tsrc_from_txt(src.c_str());\n\t\t}\n\n\t\tstd::string get_info_log() const\n\t\t{\n\t\t\tGLint max_len = 0;\n\t\t\tglGetShaderiv(m_name, GL_INFO_LOG_LENGTH, &max_len);\n\n\t\t\tstd::vector<GLchar> log(max_len);\n\t\t\tglGetShaderInfoLog(m_name, max_len, nullptr, log.data());\n\t\t\treturn std::string(log.begin(), log.end());\n\t\t}\n\n\t\tvoid compile() const\n\t\t{\n\t\t\tglCompileShader(m_name);\n\n\t\t\tGLint status;\n\t\t\tglGetShaderiv(m_name, GL_COMPILE_STATUS, &status);\n\t\t\tif (status == GL_FALSE)\n\t\t\t{\n\t\t\t\tstd::cerr << \"Failed to compile:\\n\" << get_info_log() << std::endl;\n\t\t\t\tthrow std::runtime_error(\"Couldn't compile shader. See console logs for more information.\");\n\t\t\t}\n\t\t}\n\n\t};\n\n\tstruct program\n\t{\n\t\tGLuint m_name;\n\n\t\tprogram()\n\t\t{\n\t\t\tm_name = glCreateProgram();\n\t\t}\n\n\t\t~program()\n\t\t{\n\t\t\tglDeleteProgram(m_name);\n\t\t}\n\n\t\tvoid attach_shader(GLuint shader) const\n\t\t{\n\t\t\tglAttachShader(m_name, shader);\n\t\t}\n\n\t\tstd::string get_info_log() const\n\t\t{\n\t\t\tGLint log_len = 0;\n\t\t\tglGetProgramiv(m_name, GL_INFO_LOG_LENGTH, &log_len);\n\n\t\t\tstd::vector<GLchar> log(log_len);\n\t\t\tglGetProgramInfoLog(m_name, log_len, nullptr, log.data());\n\t\t\treturn std::string(log.begin(), log.end());\n\t\t}\n\n\t\tvoid link() const\n\t\t{\n\t\t\tGLint status;\n\t\t\tglLinkProgram(m_name);\n\t\t\tglGetProgramiv(m_name, GL_LINK_STATUS, &status);\n\t\t\tif (status == GL_FALSE)\n\t\t\t{\n\t\t\t\tstd::cerr << get_info_log() << std::endl;\n\t\t\t\tthrow std::runtime_error(\"Couldn't link shader program.\");\n\t\t\t}\n\t\t}\n\n\t\tGLint get_uniform_location(const char* name) const\n\t\t{\n\t\t\tGLint loc = glGetUniformLocation(m_name, name);\n\t\t\tif (loc < 0) {\n\t\t\t\tthrow std::runtime_error(\"Couldn't find uniform. Is it unused maybe?\");\n\t\t\t}\n\t\t\treturn loc;\n\t\t}\n\n\t\tvoid use() const\n\t\t{\n\t\t\tglUseProgram(m_name);\n\t\t}\n\n\t\tstatic void unuse()\n\t\t{\n\t\t\tglUseProgram(0);\n\t\t}\n\t};\n\n\tstruct sorter\n\t{\n\tprivate:\n\t\tprogram m_count_program;\n\t\tprogram m_local_offsets_program;\n\t\tprogram m_reorder_program;\n\n\t\tsize_t m_internal_arr_len;\n\n\t\tGLuint m_local_offsets_buf;\n\t\tGLuint m_keys_scratch_buf;\n\t\tGLuint m_values_scratch_buf;\n\t\tGLuint m_glob_counts_buf;\n\n\t\tGLuint calc_thread_blocks_num(size_t arr_len)\n\t\t{\n\t\t\treturn GLuint(ceil(float(arr_len) / float(RGC_RADIX_SORT_THREADS_PER_BLOCK * RGC_RADIX_SORT_ITEMS_PER_THREAD)));\n\t\t}\n\n\t\ttemplate<typename T>\n\t\tT round_to_power_of_2(T dim)\n\t\t{\n\t\t\treturn (T) exp2(ceil(log2(dim)));\n\t\t}\n\n\t\tvoid resize_internal_buf(size_t arr_len)\n\t\t{\n\t\t\tif (m_local_offsets_buf != 0) glDeleteBuffers(1, &m_local_offsets_buf);\n\t\t\tif (m_glob_counts_buf != 0)   glDeleteBuffers(1, &m_glob_counts_buf);\n\t\t\tif (m_keys_scratch_buf != 0)  glDeleteBuffers(1, &m_keys_scratch_buf);\n\t\t\tif (m_values_scratch_buf != 0)  glDeleteBuffers(1, &m_values_scratch_buf);\n\n\t\t\tm_internal_arr_len = arr_len;\n\n\t\t\tglGenBuffers(1, &m_local_offsets_buf); // TODO TRY TO REMOVE THIS BUFFER\n\t\t\tglBindBuffer(GL_SHADER_STORAGE_BUFFER, m_local_offsets_buf);\n\t\t\tglBufferStorage(GL_SHADER_STORAGE_BUFFER, GLsizeiptr(round_to_power_of_2(calc_thread_blocks_num(arr_len)) * RGC_RADIX_SORT_BITSET_SIZE * sizeof(GLuint)), nullptr, GL_DYNAMIC_STORAGE_BIT);\n\n\t\t\tglGenBuffers(1, &m_glob_counts_buf);\n\t\t\tglBindBuffer(GL_SHADER_STORAGE_BUFFER, m_glob_counts_buf);\n\t\t\t// The spec says we don't need the GL_DYNAMIC_STORAGE_BIT.\n\t\t\t// but if we don't include it for Intel UHD graphics glClearBufferData will fail\n\t\t\tglBufferStorage(GL_SHADER_STORAGE_BUFFER, GLsizeiptr(RGC_RADIX_SORT_BITSET_SIZE * sizeof(GLuint)), nullptr, GL_DYNAMIC_STORAGE_BIT);\n\n\t\t\tglGenBuffers(1, &m_keys_scratch_buf);\n\t\t\tglBindBuffer(GL_SHADER_STORAGE_BUFFER, m_keys_scratch_buf);\n\t\t\tglBufferStorage(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr) (arr_len * sizeof(GLuint)), nullptr, 0);\n\n\t\t\tglGenBuffers(1, &m_values_scratch_buf);\n\t\t\tglBindBuffer(GL_SHADER_STORAGE_BUFFER, m_values_scratch_buf);\n\t\t\tglBufferStorage(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr) (arr_len * sizeof(GLuint)), nullptr, 0);\n\t\t}\n\n\tpublic:\n\t\tsorter(size_t init_arr_len)\n\t\t{\n\t\t\t{\n\t\t\t\tshader sh(GL_COMPUTE_SHADER);\n\t\t\t\t__rgc_shader_injector_load_src(sh.m_name, __rgc_shader_injector_shader_src_6213812085af7087c07246dabc62da60);\n\t\t\t\tsh.compile();\n\n\t\t\t\tm_count_program.attach_shader(sh.m_name);\n\t\t\t\tm_count_program.link();\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tshader sh(GL_COMPUTE_SHADER);\n\t\t\t\t__rgc_shader_injector_load_src(sh.m_name, __rgc_shader_injector_shader_src_9992419d7253eb1e5310935c952c8eff);\n\t\t\t\tsh.compile();\n\n\t\t\t\tm_local_offsets_program.attach_shader(sh.m_name);\n\t\t\t\tm_local_offsets_program.link();\n\t\t\t}\n\n\t\t\t{\n\t\t\t\tshader sh(GL_COMPUTE_SHADER);\n\t\t\t\t__rgc_shader_injector_load_src(sh.m_name, __rgc_shader_injector_shader_src_8c254d92f3f4855a006b5a5c319997b2);\n\t\t\t\tsh.compile();\n\n\t\t\t\tm_reorder_program.attach_shader(sh.m_name);\n\t\t\t\tm_reorder_program.link();\n\t\t\t}\n\n\t\t\tresize_internal_buf(init_arr_len);\n\t\t}\n\n\t\t~sorter()\n\t\t{\n\t\t\tif (m_local_offsets_buf != 0) glDeleteBuffers(1, &m_local_offsets_buf);\n\t\t\tif (m_glob_counts_buf != 0) glDeleteBuffers(1, &m_glob_counts_buf);\n\t\t\tif (m_keys_scratch_buf != 0) glDeleteBuffers(1, &m_keys_scratch_buf);\n\t\t}\n\n\t\tvoid sort(GLuint key_buf, GLuint val_buf, size_t arr_len)\n\t\t{\n\t\t\tif (arr_len <= 1) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (m_internal_arr_len < arr_len) {\n\t\t\t\tresize_internal_buf(arr_len);\n\t\t\t}\n\n\t\t\t// ------------------------------------------------------------------------------------------------\n\t\t\t// Sorting\n\t\t\t// ------------------------------------------------------------------------------------------------\n\n\t\t\tGLuint thread_blocks_num = calc_thread_blocks_num(arr_len);\n\t\t\tGLuint power_of_two_thread_blocks_num = round_to_power_of_2(thread_blocks_num);\n\n\t\t\tGLuint keys_buffers[2] = {key_buf, m_keys_scratch_buf};\n\t\t\tGLuint values_buffers[2] = {val_buf, m_values_scratch_buf};\n\n\t\t\tfor (GLuint pass = 0; pass < RGC_RADIX_SORT_BITSET_COUNT; pass++)\n\t\t\t{\n\t\t\t\t// ------------------------------------------------------------------------------------------------\n\t\t\t\t// Initial clearing\n\t\t\t\t// ------------------------------------------------------------------------------------------------\n\n\t\t\t\tglBindBuffer(GL_SHADER_STORAGE_BUFFER, m_glob_counts_buf);\n\n#ifdef __ANDROID__\n\t\t\t\tsize_t glob_counts_size = RGC_RADIX_SORT_BITSET_SIZE * sizeof(GLuint);\n\t\t\t\tZeroBuffer(GL_SHADER_STORAGE_BUFFER, glob_counts_size);\n#else\n\t\t\t\tglClearBufferData(GL_SHADER_STORAGE_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, &k_zero);\n#endif\n\n\t\t\t\tglBindBuffer(GL_SHADER_STORAGE_BUFFER, m_local_offsets_buf);\n#ifdef __ANDROID__\n\t\t\t\tsize_t local_offsets_size = round_to_power_of_2(calc_thread_blocks_num(arr_len)) * RGC_RADIX_SORT_BITSET_SIZE * sizeof(GLuint);\n\t\t\t\tZeroBuffer(GL_SHADER_STORAGE_BUFFER, local_offsets_size);\n#else\n\t\t\t\tglClearBufferData(GL_SHADER_STORAGE_BUFFER, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, &k_zero);\n#endif\n\n\t\t\t\t// ------------------------------------------------------------------------------------------------\n\t\t\t\t// Counting\n\t\t\t\t// ------------------------------------------------------------------------------------------------\n\n\t\t\t\t// Per-block & global radix count\n\n\t\t\t\tm_count_program.use();\n\n\t\t\t\tglBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keys_buffers[pass % 2]);\n\t\t\t\tglBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_local_offsets_buf);\n\t\t\t\tglBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, m_glob_counts_buf);\n\n\t\t\t\tglUniform1ui(m_count_program.get_uniform_location(\"u_arr_len\"), (GLuint)arr_len);\n\t\t\t\tglUniform1ui(m_count_program.get_uniform_location(\"u_bitset_idx\"), pass);\n\n\t\t\t\tRGC_RADIX_SORT_RENDERDOC_WATCH(true, [&]()\n\t\t\t\t{\n\t\t\t\t\tglDispatchCompute(thread_blocks_num, 1, 1);\n\t\t\t\t\tglMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);\n\t\t\t\t});\n\n\t\t\t\t// ------------------------------------------------------------------------------------------------\n\t\t\t\t// Local offsets (block-wide exclusive scan per radix)\n\t\t\t\t// ------------------------------------------------------------------------------------------------\n\n\t\t\t\tm_local_offsets_program.use();\n\n\t\t\t\tglBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_local_offsets_buf);\n\n\t\t\t\t// Up-sweep (reduction)\n\t\t\t\tfor (GLuint d = 0; d < GLuint(log2(power_of_two_thread_blocks_num)); d++)\n\t\t\t\t{\n\t\t\t\t\tglUniform1ui(m_local_offsets_program.get_uniform_location(\"u_arr_len\"), power_of_two_thread_blocks_num);\n\t\t\t\t\tglUniform1ui(m_local_offsets_program.get_uniform_location(\"u_op\"), 0);\n\t\t\t\t\tglUniform1ui(m_local_offsets_program.get_uniform_location(\"u_depth\"), d);\n\n\t\t\t\t\tRGC_RADIX_SORT_RENDERDOC_WATCH(false, [&]()\n\t\t\t\t\t{\n\t\t\t\t\t\tauto workgroups_num = GLuint(ceil(float(power_of_two_thread_blocks_num) / float(RGC_RADIX_SORT_THREADS_PER_BLOCK * RGC_RADIX_SORT_ITEMS_PER_THREAD)));\n\t\t\t\t\t\tglDispatchCompute(workgroups_num, 1, 1);\n\t\t\t\t\t\tglMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\t// Clear last\n\t\t\t\tglUniform1ui(m_local_offsets_program.get_uniform_location(\"u_arr_len\"), power_of_two_thread_blocks_num);\n\t\t\t\tglUniform1ui(m_local_offsets_program.get_uniform_location(\"u_op\"), 1);\n\n\t\t\t\tRGC_RADIX_SORT_RENDERDOC_WATCH(false, [&]()\n\t\t\t\t{\n\t\t\t\t\tauto workgroups_num = GLuint(ceil(float(power_of_two_thread_blocks_num) / float(RGC_RADIX_SORT_THREADS_PER_BLOCK * RGC_RADIX_SORT_ITEMS_PER_THREAD)));\n\t\t\t\t\tglDispatchCompute(workgroups_num, 1, 1);\n\t\t\t\t\tglMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);\n\t\t\t\t});\n\n\t\t\t\t// Down-sweep\n\t\t\t\tfor (GLint d = GLint(log2(power_of_two_thread_blocks_num)) - 1; d >= 0; d--)\n\t\t\t\t{\n\t\t\t\t\tglUniform1ui(m_local_offsets_program.get_uniform_location(\"u_arr_len\"), power_of_two_thread_blocks_num);\n\t\t\t\t\tglUniform1ui(m_local_offsets_program.get_uniform_location(\"u_op\"), 2);\n\t\t\t\t\tglUniform1ui(m_local_offsets_program.get_uniform_location(\"u_depth\"), d);\n\n\t\t\t\t\tRGC_RADIX_SORT_RENDERDOC_WATCH(false, [&]()\n\t\t\t\t\t{\n\t\t\t\t\t\tauto workgroups_num = GLuint(ceil(float(power_of_two_thread_blocks_num) / float(RGC_RADIX_SORT_THREADS_PER_BLOCK * RGC_RADIX_SORT_ITEMS_PER_THREAD)));\n\t\t\t\t\t\tglDispatchCompute(workgroups_num, 1, 1);\n\t\t\t\t\t\tglMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\t// ------------------------------------------------------------------------------------------------\n\t\t\t\t// Reordering\n\t\t\t\t// ------------------------------------------------------------------------------------------------\n\n\t\t\t\t// In thread-block reordering & writes to global memory\n\n\t\t\t\tm_reorder_program.use();\n\n\t\t\t\tglBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, keys_buffers[pass % 2], 0, (GLsizeiptr) (arr_len * sizeof(GLuint)));\n\t\t\t\tglBindBufferRange(GL_SHADER_STORAGE_BUFFER, 1, keys_buffers[(pass + 1) % 2], 0, (GLsizeiptr) (arr_len * sizeof(GLuint)));\n\n\t\t\t\tif (val_buf != 0)\n\t\t\t\t{\n\t\t\t\t\tglBindBufferRange(GL_SHADER_STORAGE_BUFFER, 2, values_buffers[pass % 2], 0, (GLsizeiptr) (arr_len * sizeof(GLuint)));\n\t\t\t\t\tglBindBufferRange(GL_SHADER_STORAGE_BUFFER, 3, values_buffers[(pass + 1) % 2], 0, (GLsizeiptr) (arr_len * sizeof(GLuint)));\n\t\t\t\t}\n\n\t\t\t\tglBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, m_local_offsets_buf);\n\t\t\t\tglBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, m_glob_counts_buf);\n\n\t\t\t\tglUniform1ui(m_reorder_program.get_uniform_location(\"u_write_values\"), val_buf != 0);\n\t\t\t\tglUniform1ui(m_reorder_program.get_uniform_location(\"u_arr_len\"), (GLuint)arr_len);\n\t\t\t\tglUniform1ui(m_reorder_program.get_uniform_location(\"u_bitset_idx\"), pass);\n\n\t\t\t\tRGC_RADIX_SORT_RENDERDOC_WATCH(true, [&]()\n\t\t\t\t{\n\t\t\t\t\tglDispatchCompute(thread_blocks_num, 1, 1);\n\t\t\t\t\tglMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tprogram::unuse();\n\t\t}\n\t};\n}\n\n"
  },
  {
    "path": "src/sdl_main.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n// 3d gaussian splat renderer\n\n#include <chrono>\n#include <GL/glew.h>\n#include <glm/glm.hpp>\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_opengl.h>\n#include <SDL2/SDL_syswm.h>\n#include <stdint.h>\n#include <thread>\n\n#ifdef TRACY_ENABLE\n#include <tracy/Tracy.hpp>\n#else\n#define FrameMark do {} while(0)\n#endif\n\n#include \"core/log.h\"\n#include \"core/util.h\"\n\n#include \"app.h\"\n\n//#define SOFTWARE_SPLATS\n\nstruct GlobalContext\n{\n    bool quitting = false;\n    SDL_Window* window = NULL;\n    SDL_GLContext gl_context;\n};\n\nGlobalContext ctx;\n\nint SDLCALL Watch(void *userdata, SDL_Event* event)\n{\n    if (event->type == SDL_APP_WILLENTERBACKGROUND)\n    {\n        ctx.quitting = true;\n    }\n\n    return 1;\n}\n\nint main(int argc, char *argv[])\n{\n    Log::SetAppName(\"splataplut\");\n    MainContext mainContext;\n    App app(mainContext);\n    App::ParseResult parseResult = app.ParseArguments(argc, (const char**)argv);\n    switch (parseResult)\n    {\n    case App::SUCCESS_RESULT:\n        break;\n    case App::ERROR_RESULT:\n        Log::E(\"App::ParseArguments failed!\\n\");\n        return 1;\n    case App::QUIT_RESULT:\n        return 0;\n    }\n\n    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_JOYSTICK) != 0)\n    {\n        Log::E(\"Failed to initialize SDL: %s\\n\", SDL_GetError());\n        return 1;\n    }\n\n    const int32_t WIDTH = 1024;\n    const int32_t HEIGHT = 768;\n\n    // Allow us to use automatic linear->sRGB conversion.\n    SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, 1);\n\n    // increase depth buffer size\n    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);\n\n    uint32_t windowFlags = SDL_WINDOW_OPENGL;\n    if (app.IsFullscreen())\n    {\n        windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;\n    }\n    else\n    {\n        windowFlags |= SDL_WINDOW_RESIZABLE;\n    }\n    ctx.window = SDL_CreateWindow(\"splatapult\", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WIDTH, HEIGHT, windowFlags);\n\n    if (!ctx.window)\n    {\n        Log::E(\"Failed to create window: %s\\n\", SDL_GetError());\n        return 1;\n    }\n\n    ctx.gl_context = SDL_GL_CreateContext(ctx.window);\n\n    SDL_GL_MakeCurrent(ctx.window, ctx.gl_context);\n\n#ifdef __linux__\n    // Initialize context from the SDL window\n    SDL_SysWMinfo info;\n    SDL_VERSION(&info.version)\n    auto ret = SDL_GetWindowWMInfo(ctx.window, &info);\n    if (ret != SDL_TRUE)\n    {\n        Log::W(\"Failed to retrieve SDL window info: %s\\n\", SDL_GetError());\n    }\n    else\n    {\n        mainContext.xdisplay = info.info.x11.display;\n        mainContext.glxDrawable = (GLXWindow)info.info.x11.window;\n        mainContext.glxContext = (GLXContext)ctx.gl_context;\n    }\n#endif\n\n    GLenum err = glewInit();\n    if (GLEW_OK != err)\n    {\n        Log::E(\"Error: %s\\n\", glewGetErrorString(err));\n        return 1;\n    }\n\n    // AJT: TODO REMOVE disable vsync for benchmarks\n    SDL_GL_SetSwapInterval(0);\n\n    SDL_AddEventWatch(Watch, NULL);\n\n    if (!app.Init())\n    {\n        Log::E(\"App::Init failed\\n\");\n        return 1;\n    }\n\n    bool shouldQuit = false;\n    app.OnQuit([&shouldQuit]()\n    {\n        shouldQuit = true;\n    });\n\n    app.OnResize([](int newWidth, int newHeight)\n    {\n        //SDL_RenderSetLogicalSize(ctx.renderer, newWidth, newHeight);\n        // AJT: TODO resize texture?\n    });\n\n    uint32_t frameCount = 1;\n    uint32_t frameTicks = SDL_GetTicks();\n    uint32_t lastTicks = SDL_GetTicks();\n    while (!ctx.quitting && !shouldQuit)\n    {\n        // update dt\n        uint32_t ticks = SDL_GetTicks();\n\n        const int FPS_FRAMES = 100;\n        if ((frameCount % FPS_FRAMES) == 0)\n        {\n            float delta = (ticks - frameTicks) / 1000.0f;\n            float fps = (float)FPS_FRAMES / delta;\n            frameTicks = ticks;\n            app.UpdateFps(fps);\n        }\n        float dt = (ticks - lastTicks) / 1000.0f;\n        lastTicks = ticks;\n\n        SDL_Event event;\n        while (SDL_PollEvent(&event))\n        {\n            app.ProcessEvent(event);\n        }\n\n        if (!app.Process(dt))\n        {\n            Log::E(\"App::Process failed!\\n\");\n            return 1;\n        }\n\n        SDL_GL_MakeCurrent(ctx.window, ctx.gl_context);\n\n        int width, height;\n        SDL_GetWindowSize(ctx.window, &width, &height);\n        if (!app.Render(dt, glm::ivec2(width, height)))\n        {\n            Log::E(\"App::Render failed!\\n\");\n            return 1;\n        }\n\n        SDL_GL_SwapWindow(ctx.window);\n\n        frameCount++;\n\n        FrameMark;\n    }\n\n    SDL_DelEventWatch(Watch, NULL);\n    SDL_GL_DeleteContext(ctx.gl_context);\n\n    SDL_DestroyWindow(ctx.window);\n    SDL_Quit();\n\n    return 0;\n}\n"
  },
  {
    "path": "src/splatrenderer.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"splatrenderer.h\"\n\n#ifdef __ANDROID__\n#include <EGL/egl.h>\n#include <EGL/eglext.h>\n#include <GLES3/gl3.h>\n#include <GLES3/gl3ext.h>\n#else\n#include <GL/glew.h>\n#endif\n\n#include <glm/gtc/matrix_transform.hpp>\n\n#ifdef TRACY_ENABLE\n#include <tracy/Tracy.hpp>\n#else\n#define ZoneScoped\n#define ZoneScopedNC(NAME, COLOR)\n#endif\n\n#include \"core/image.h\"\n#include \"core/log.h\"\n#include \"core/texture.h\"\n#include \"core/util.h\"\n\n#include \"radix_sort.hpp\"\n\nstatic const uint32_t NUM_BLOCKS_PER_WORKGROUP = 1024;\n\nstatic void SetupAttrib(int loc, const BinaryAttribute& attrib, int32_t count, size_t stride)\n{\n    assert(attrib.type == BinaryAttribute::Type::Float);\n    glVertexAttribPointer(loc, count, GL_FLOAT, GL_FALSE, (uint32_t)stride, (void*)attrib.offset);\n    glEnableVertexAttribArray(loc);\n}\n\nSplatRenderer::SplatRenderer()\n{\n}\n\nSplatRenderer::~SplatRenderer()\n{\n}\n\nbool SplatRenderer::Init(std::shared_ptr<GaussianCloud> gaussianCloud,\n                         bool isFramebufferSRGBEnabledIn, bool useRgcSortOverrideIn)\n{\n    ZoneScopedNC(\"SplatRenderer::Init()\", tracy::Color::Blue);\n    GL_ERROR_CHECK(\"SplatRenderer::Init() begin\");\n\n    isFramebufferSRGBEnabled = isFramebufferSRGBEnabledIn;\n    useRgcSortOverride = useRgcSortOverrideIn;\n\n    splatProg = std::make_shared<Program>();\n    if (isFramebufferSRGBEnabled || gaussianCloud->HasFullSH())\n    {\n        std::string defines = \"\";\n        if (isFramebufferSRGBEnabled)\n        {\n            defines += \"#define FRAMEBUFFER_SRGB\\n\";\n        }\n        if (gaussianCloud->HasFullSH())\n        {\n            defines += \"#define FULL_SH\\n\";\n        }\n        splatProg->AddMacro(\"DEFINES\", defines);\n    }\n    if (!splatProg->LoadVertGeomFrag(\"shader/splat_vert.glsl\", \"shader/splat_geom.glsl\", \"shader/splat_frag.glsl\"))\n    {\n        Log::E(\"Error loading splat shaders!\\n\");\n        return false;\n    }\n\n    preSortProg = std::make_shared<Program>();\n    if (!preSortProg->LoadCompute(\"shader/presort_compute.glsl\"))\n    {\n        Log::E(\"Error loading pre-sort compute shader!\\n\");\n        return false;\n    }\n\n    bool useMultiRadixSort = GLEW_KHR_shader_subgroup && !useRgcSortOverride;\n\n    if (useMultiRadixSort)\n    {\n        sortProg = std::make_shared<Program>();\n        if (!sortProg->LoadCompute(\"shader/multi_radixsort.glsl\"))\n        {\n            Log::E(\"Error loading sort compute shader!\\n\");\n            return false;\n        }\n\n        histogramProg = std::make_shared<Program>();\n        if (!histogramProg->LoadCompute(\"shader/multi_radixsort_histograms.glsl\"))\n        {\n            Log::E(\"Error loading histogram compute shader!\\n\");\n            return false;\n        }\n    }\n\n    // build posVec\n    size_t numGaussians = gaussianCloud->GetNumGaussians();\n    posVec.reserve(numGaussians);\n    gaussianCloud->ForEachPosWithAlpha([this](const float* pos)\n    {\n        posVec.emplace_back(glm::vec4(pos[0], pos[1], pos[2], 1.0f));\n    });\n\n    BuildVertexArrayObject(gaussianCloud);\n\n    depthVec.resize(numGaussians);\n\n    if (useMultiRadixSort)\n    {\n        Log::I(\"using multi_radixsort.glsl\\n\");\n\n        keyBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, depthVec, GL_DYNAMIC_STORAGE_BIT);\n        keyBuffer2 = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, depthVec, GL_DYNAMIC_STORAGE_BIT);\n\n        const uint32_t NUM_ELEMENTS = static_cast<uint32_t>(numGaussians);\n        const uint32_t NUM_WORKGROUPS = (NUM_ELEMENTS + numBlocksPerWorkgroup - 1) / numBlocksPerWorkgroup;\n        const uint32_t RADIX_SORT_BINS = 256;\n\n        std::vector<uint32_t> histogramVec(NUM_WORKGROUPS * RADIX_SORT_BINS, 0);\n        histogramBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, histogramVec, GL_DYNAMIC_STORAGE_BIT);\n\n        valBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT);\n        valBuffer2 = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT);\n        posBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, posVec);\n    }\n    else\n    {\n        Log::I(\"using rgc::radix_sort\\n\");\n        keyBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, depthVec, GL_DYNAMIC_STORAGE_BIT);\n        valBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT);\n        posBuffer = std::make_shared<BufferObject>(GL_SHADER_STORAGE_BUFFER, posVec);\n\n        sorter = std::make_shared<rgc::radix_sort::sorter>(numGaussians);\n    }\n\n    atomicCounterVec.resize(1, 0);\n    atomicCounterBuffer = std::make_shared<BufferObject>(GL_ATOMIC_COUNTER_BUFFER, atomicCounterVec, GL_DYNAMIC_STORAGE_BIT | GL_MAP_READ_BIT);\n\n    GL_ERROR_CHECK(\"SplatRenderer::Init() end\");\n\n    return true;\n}\n\nvoid SplatRenderer::Sort(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                         const glm::vec4& viewport, const glm::vec2& nearFar)\n{\n    ZoneScoped;\n\n    GL_ERROR_CHECK(\"SplatRenderer::Sort() begin\");\n\n    const size_t numPoints = posVec.size();\n    glm::mat4 modelViewMat = glm::inverse(cameraMat);\n\n    bool useMultiRadixSort = GLEW_KHR_shader_subgroup && !useRgcSortOverride;\n\n    // 24 bit radix sort still has some artifacts on some datasets, so use 32 bit sort.\n    //const uint32_t NUM_BYTES = useMultiRadixSort ? 3 : 4;\n    //const uint32_t MAX_DEPTH = useMultiRadixSort ? 16777215 : std::numeric_limits<uint32_t>::max();\n    const uint32_t NUM_BYTES = 4;\n    const uint32_t MAX_DEPTH = std::numeric_limits<uint32_t>::max();\n\n    {\n        ZoneScopedNC(\"pre-sort\", tracy::Color::Red4);\n\n        preSortProg->Bind();\n        preSortProg->SetUniform(\"modelViewProj\", projMat * modelViewMat);\n        preSortProg->SetUniform(\"nearFar\", nearFar);\n        preSortProg->SetUniform(\"keyMax\", MAX_DEPTH);\n\n        // reset counter back to zero\n        atomicCounterVec[0] = 0;\n        atomicCounterBuffer->Update(atomicCounterVec);\n\n        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, posBuffer->GetObj());  // readonly\n        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, keyBuffer->GetObj());  // writeonly\n        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, valBuffer->GetObj());  // writeonly\n        glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 4, atomicCounterBuffer->GetObj());\n\n        const int LOCAL_SIZE = 256;\n        glDispatchCompute(((GLuint)numPoints + (LOCAL_SIZE - 1)) / LOCAL_SIZE, 1, 1); // Assuming LOCAL_SIZE threads per group\n        glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT);\n\n        GL_ERROR_CHECK(\"SplatRenderer::Sort() pre-sort\");\n    }\n\n    {\n        ZoneScopedNC(\"get-count\", tracy::Color::Green);\n\n        atomicCounterBuffer->Read(atomicCounterVec);\n        sortCount = atomicCounterVec[0];\n\n        assert(sortCount <= (uint32_t)numPoints);\n\n        GL_ERROR_CHECK(\"SplatRenderer::Render() get-count\");\n    }\n\n    if (useMultiRadixSort)\n    {\n        ZoneScopedNC(\"sort\", tracy::Color::Red4);\n\n        const uint32_t NUM_ELEMENTS = static_cast<uint32_t>(sortCount);\n        const uint32_t NUM_WORKGROUPS = (NUM_ELEMENTS + numBlocksPerWorkgroup - 1) / numBlocksPerWorkgroup;\n\n        sortProg->Bind();\n        sortProg->SetUniform(\"g_num_elements\", NUM_ELEMENTS);\n        sortProg->SetUniform(\"g_num_workgroups\", NUM_WORKGROUPS);\n        sortProg->SetUniform(\"g_num_blocks_per_workgroup\", numBlocksPerWorkgroup);\n\n        histogramProg->Bind();\n        histogramProg->SetUniform(\"g_num_elements\", NUM_ELEMENTS);\n        //histogramProg->SetUniform(\"g_num_workgroups\", NUM_WORKGROUPS);\n        histogramProg->SetUniform(\"g_num_blocks_per_workgroup\", numBlocksPerWorkgroup);\n\n        for (uint32_t i = 0; i < NUM_BYTES; i++)\n        {\n            histogramProg->Bind();\n            histogramProg->SetUniform(\"g_shift\", 8 * i);\n\n            if (i == 0 || i == 2)\n            {\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keyBuffer->GetObj());\n            }\n            else\n            {\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keyBuffer2->GetObj());\n            }\n            glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, histogramBuffer->GetObj());\n\n            glDispatchCompute(NUM_WORKGROUPS, 1, 1);\n\n            glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);\n\n            sortProg->Bind();\n            sortProg->SetUniform(\"g_shift\", 8 * i);\n\n            if ((i % 2) == 0)  // even\n            {\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keyBuffer->GetObj());\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, keyBuffer2->GetObj());\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, valBuffer->GetObj());\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, valBuffer2->GetObj());\n            }\n            else  // odd\n            {\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, keyBuffer2->GetObj());\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, keyBuffer->GetObj());\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, valBuffer2->GetObj());\n                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, valBuffer->GetObj());\n            }\n            glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, histogramBuffer->GetObj());\n\n            glDispatchCompute(NUM_WORKGROUPS, 1, 1);\n\n            glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);\n        }\n\n        GL_ERROR_CHECK(\"SplatRenderer::Sort() sort\");\n\n        // indicate if keys are sorted properly or not.\n        if (false)\n        {\n            std::vector<uint32_t> sortedKeyVec(numPoints, 0);\n            keyBuffer->Read(sortedKeyVec);\n\n            GL_ERROR_CHECK(\"SplatRenderer::Sort() READ buffer\");\n\n            bool sorted = true;\n            for (uint32_t i = 1; i < sortCount; i++)\n            {\n                if (sortedKeyVec[i - 1] > sortedKeyVec[i])\n                {\n                    sorted = false;\n                    break;\n                }\n            }\n\n            printf(\"%s\", sorted ? \"o\" : \"x\");\n        }\n    }\n    else\n    {\n        ZoneScopedNC(\"sort\", tracy::Color::Red4);\n        sorter->sort(keyBuffer->GetObj(), valBuffer->GetObj(), sortCount);\n        GL_ERROR_CHECK(\"SplatRenderer::Sort() rgc sort\");\n    }\n\n    {\n        ZoneScopedNC(\"copy-sorted\", tracy::Color::DarkGreen);\n\n        if (useMultiRadixSort && (NUM_BYTES % 2) == 1)  // odd\n        {\n            glBindBuffer(GL_COPY_READ_BUFFER, valBuffer2->GetObj());\n        }\n        else\n        {\n            glBindBuffer(GL_COPY_READ_BUFFER, valBuffer->GetObj());\n        }\n        glBindBuffer(GL_COPY_WRITE_BUFFER, splatVao->GetElementBuffer()->GetObj());\n        glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, sortCount * sizeof(uint32_t));\n\n        GL_ERROR_CHECK(\"SplatRenderer::Sort() copy-sorted\");\n    }\n}\n\n\nvoid SplatRenderer::Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                           const glm::vec4& viewport, const glm::vec2& nearFar)\n{\n    ZoneScoped;\n\n    GL_ERROR_CHECK(\"SplatRenderer::Render() begin\");\n\n    {\n        ZoneScopedNC(\"draw\", tracy::Color::Red4);\n        float width = viewport.z;\n        float height = viewport.w;\n        float aspectRatio = width / height;\n        glm::mat4 viewMat = glm::inverse(cameraMat);\n        glm::vec3 eye = glm::vec3(cameraMat[3]);\n\n        splatProg->Bind();\n        splatProg->SetUniform(\"viewMat\", viewMat);\n        splatProg->SetUniform(\"projMat\", projMat);\n        splatProg->SetUniform(\"viewport\", viewport);\n        splatProg->SetUniform(\"projParams\", glm::vec4(0.0f, nearFar.x, nearFar.y, 0.0f));\n        splatProg->SetUniform(\"eye\", eye);\n\n        splatVao->Bind();\n        glDrawElements(GL_POINTS, sortCount, GL_UNSIGNED_INT, nullptr);\n        splatVao->Unbind();\n\n        GL_ERROR_CHECK(\"SplatRenderer::Render() draw\");\n    }\n}\n\nvoid SplatRenderer::BuildVertexArrayObject(std::shared_ptr<GaussianCloud> gaussianCloud)\n{\n    splatVao = std::make_shared<VertexArrayObject>();\n\n    // allocate large buffer to hold interleaved vertex data\n    gaussianDataBuffer = std::make_shared<BufferObject>(GL_ARRAY_BUFFER,\n                                                        gaussianCloud->GetRawDataPtr(),\n                                                        gaussianCloud->GetTotalSize(), 0);\n\n    const size_t numGaussians = gaussianCloud->GetNumGaussians();\n\n    // build element array\n    indexVec.reserve(numGaussians);\n    assert(numGaussians <= std::numeric_limits<uint32_t>::max());\n    for (uint32_t i = 0; i < (uint32_t)numGaussians; i++)\n    {\n        indexVec.push_back(i);\n    }\n    auto indexBuffer = std::make_shared<BufferObject>(GL_ELEMENT_ARRAY_BUFFER, indexVec, GL_DYNAMIC_STORAGE_BIT);\n\n    splatVao->Bind();\n    gaussianDataBuffer->Bind();\n\n    const size_t stride = gaussianCloud->GetStride();\n    SetupAttrib(splatProg->GetAttribLoc(\"position\"), gaussianCloud->GetPosWithAlphaAttrib(), 4, stride);\n    SetupAttrib(splatProg->GetAttribLoc(\"r_sh0\"), gaussianCloud->GetR_SH0Attrib(), 4, stride);\n    SetupAttrib(splatProg->GetAttribLoc(\"g_sh0\"), gaussianCloud->GetG_SH0Attrib(), 4, stride);\n    SetupAttrib(splatProg->GetAttribLoc(\"b_sh0\"), gaussianCloud->GetB_SH0Attrib(), 4, stride);\n    if (gaussianCloud->HasFullSH())\n    {\n        SetupAttrib(splatProg->GetAttribLoc(\"r_sh1\"), gaussianCloud->GetR_SH1Attrib(), 4, stride);\n        SetupAttrib(splatProg->GetAttribLoc(\"r_sh2\"), gaussianCloud->GetR_SH2Attrib(), 4, stride);\n        SetupAttrib(splatProg->GetAttribLoc(\"r_sh3\"), gaussianCloud->GetR_SH3Attrib(), 4, stride);\n        SetupAttrib(splatProg->GetAttribLoc(\"g_sh1\"), gaussianCloud->GetG_SH1Attrib(), 4, stride);\n        SetupAttrib(splatProg->GetAttribLoc(\"g_sh2\"), gaussianCloud->GetG_SH2Attrib(), 4, stride);\n        SetupAttrib(splatProg->GetAttribLoc(\"g_sh3\"), gaussianCloud->GetG_SH3Attrib(), 4, stride);\n        SetupAttrib(splatProg->GetAttribLoc(\"b_sh1\"), gaussianCloud->GetB_SH1Attrib(), 4, stride);\n        SetupAttrib(splatProg->GetAttribLoc(\"b_sh2\"), gaussianCloud->GetB_SH2Attrib(), 4, stride);\n        SetupAttrib(splatProg->GetAttribLoc(\"b_sh3\"), gaussianCloud->GetB_SH3Attrib(), 4, stride);\n    }\n    SetupAttrib(splatProg->GetAttribLoc(\"cov3_col0\"), gaussianCloud->GetCov3_Col0Attrib(), 3, stride);\n    SetupAttrib(splatProg->GetAttribLoc(\"cov3_col1\"), gaussianCloud->GetCov3_Col1Attrib(), 3, stride);\n    SetupAttrib(splatProg->GetAttribLoc(\"cov3_col2\"), gaussianCloud->GetCov3_Col2Attrib(), 3, stride);\n\n    splatVao->SetElementBuffer(indexBuffer);\n    gaussianDataBuffer->Unbind();\n}\n"
  },
  {
    "path": "src/splatrenderer.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <memory>\n#include <stdint.h>\n#include <vector>\n\n#include \"core/program.h\"\n#include \"core/vertexbuffer.h\"\n\n#include \"gaussiancloud.h\"\n\nnamespace rgc::radix_sort\n{\n    struct sorter;\n}\n\nclass SplatRenderer\n{\npublic:\n    SplatRenderer();\n    ~SplatRenderer();\n\n    bool Init(std::shared_ptr<GaussianCloud> gaussianCloud,\n              bool isFramebufferSRGBEnabledIn, bool useRgcSortOverrideIn);\n\n    void Sort(const glm::mat4& cameraMat, const glm::mat4& projMat,\n              const glm::vec4& viewport, const glm::vec2& nearFar);\n\n    // viewport = (x, y, width, height)\n    void Render(const glm::mat4& cameraMat, const glm::mat4& projMat,\n                const glm::vec4& viewport, const glm::vec2& nearFar);\npublic:\n    uint32_t numBlocksPerWorkgroup = 1024;\nprotected:\n    void BuildVertexArrayObject(std::shared_ptr<GaussianCloud> gaussianCloud);\n\n    std::shared_ptr<rgc::radix_sort::sorter> sorter;\n    std::shared_ptr<Program> splatProg;\n    std::shared_ptr<Program> preSortProg;\n    std::shared_ptr<Program> histogramProg;\n    std::shared_ptr<Program> sortProg;\n    std::shared_ptr<VertexArrayObject> splatVao;\n\n    std::vector<uint32_t> indexVec;\n    std::vector<uint32_t> depthVec;\n    std::vector<glm::vec4> posVec;\n    std::vector<uint32_t> atomicCounterVec;\n\n    std::shared_ptr<BufferObject> gaussianDataBuffer;\n    std::shared_ptr<BufferObject> keyBuffer;\n    std::shared_ptr<BufferObject> keyBuffer2;\n    std::shared_ptr<BufferObject> histogramBuffer;\n    std::shared_ptr<BufferObject> valBuffer;\n    std::shared_ptr<BufferObject> valBuffer2;\n    std::shared_ptr<BufferObject> posBuffer;\n    std::shared_ptr<BufferObject> atomicCounterBuffer;\n\n    uint32_t sortCount;\n    bool isFramebufferSRGBEnabled;\n    bool useRgcSortOverride;\n};\n"
  },
  {
    "path": "src/vrconfig.cpp",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#include \"vrconfig.h\"\n\n#include <fstream>\n#include <nlohmann/json.hpp>\n#include <iostream>\n\n#include \"core/log.h\"\n#include \"core/util.h\"\n\nVrConfig::VrConfig() : floorMat(1.0f)\n{\n    ;\n}\n\nbool VrConfig::ImportJson(const std::string& jsonFilename)\n{\n    std::ifstream f(jsonFilename);\n    if (f.fail())\n    {\n        return false;\n    }\n\n    try\n    {\n        nlohmann::json obj = nlohmann::json::parse(f);\n        nlohmann::json jmat = obj[\"floorMat\"];\n        glm::mat4 mat(jmat[0][0].template get<float>(), jmat[1][0].template get<float>(), jmat[2][0].template get<float>(), jmat[3][0].template get<float>(),\n                      jmat[0][1].template get<float>(), jmat[1][1].template get<float>(), jmat[2][1].template get<float>(), jmat[3][1].template get<float>(),\n                      jmat[0][2].template get<float>(), jmat[1][2].template get<float>(), jmat[2][2].template get<float>(), jmat[3][2].template get<float>(),\n                      jmat[0][3].template get<float>(), jmat[1][3].template get<float>(), jmat[2][3].template get<float>(), jmat[3][3].template get<float>());\n        floorMat = mat;\n    }\n    catch (const nlohmann::json::exception& e)\n    {\n        std::string s = e.what();\n        Log::E(\"VrConfig::ImportJson exception: %s\\n\", s.c_str());\n        return false;\n    }\n\n    return true;\n}\n\nbool VrConfig::ExportJson(const std::string& jsonFilename) const\n{\n    std::ofstream f(jsonFilename);\n    if (f.fail())\n    {\n        return false;\n    }\n\n    f << \"{\" << std::endl;\n    f << \"    \\\"floorMat\\\": [\";\n    f << \"[\" << floorMat[0][0] << \", \" << floorMat[1][0] << \", \" << floorMat[2][0] << \", \" << floorMat[3][0] << \"], \";\n    f << \"[\" << floorMat[0][1] << \", \" << floorMat[1][1] << \", \" << floorMat[2][1] << \", \" << floorMat[3][1] << \"], \";\n    f << \"[\" << floorMat[0][2] << \", \" << floorMat[1][2] << \", \" << floorMat[2][2] << \", \" << floorMat[3][2] << \"], \";\n    f << \"[\" << floorMat[0][3] << \", \" << floorMat[1][3] << \", \" << floorMat[2][3] << \", \" << floorMat[3][3] << \"]]\";\n    f << std::endl << \"}\";\n\n    return true;\n}\n"
  },
  {
    "path": "src/vrconfig.h",
    "content": "/*\n    Copyright (c) 2024 Anthony J. Thibault\n    This software is licensed under the MIT License. See LICENSE for more details.\n*/\n\n#pragma once\n\n#include <glm/glm.hpp>\n#include <string>\n#include <vector>\n\nclass VrConfig\n{\npublic:\n    VrConfig();\n\n    bool ImportJson(const std::string& jsonFilename);\n    bool ExportJson(const std::string& jsonFilename) const;\n    const glm::mat4& GetFloorMat() const { return floorMat; }\n    void SetFloorMat(const glm::mat4& floorMatIn) { floorMat = floorMatIn; }\nprotected:\n    glm::mat4 floorMat;\n};\n"
  },
  {
    "path": "tasks.py",
    "content": "from invoke import task\nimport os\nimport shutil\n\n\nRELEASE_NAME = \"splatapult-0.1-x64\"\n\n@task\ndef clean(c):\n    c.run(\"rm -rf build\")\n\ndef build_with_config(c, config, options={}):\n    if (not os.path.exists(\"build\")):\n        c.run(\"mkdir build\")\n\n    with c.cd(\"build\"):\n        defines = []\n        for k, v in options.items():\n            defines.append(f\"-D{k}={v}\")\n        c.run(f'cmake {\" \".join(defines)} -DCMAKE_TOOLCHAIN_FILE=\"../vcpkg/scripts/buildsystems/vcpkg.cmake\" ..')\n        c.run(f\"cmake --build . --config={config}\")\n\n@task\ndef build(c):\n    build_with_config(c, \"Release\", {\"SHIPPING\": \"ON\"})\n\n@task\ndef build_debug(c):\n    build_with_config(c, \"Debug\")\n\n@task\ndef archive(c):\n    shutil.make_archive(RELEASE_NAME, \"zip\", \"build/Release\")\n\n@task\ndef deploy(c):\n    c.run(f\"cp {RELEASE_NAME}.zip ../hyperlogic.github.io/files\")\n    with c.cd(\"../hyperlogic.github.io\"):\n        c.run(f\"git add files/{RELEASE_NAME}.zip\")\n        c.run(f'git commit -m \"Automated deploy of {RELEASE_NAME}\"')\n        c.run(\"git push\")\n\n\n@task\ndef all(c):\n    clean(c)\n    build(c)\n    archive(c)\n    deploy(c)\n"
  },
  {
    "path": "vcpkg.json",
    "content": "{\n  \"name\": \"main\",\n  \"version-string\": \"latest\",\n  \"dependencies\": [\n    \"sdl2\",\n    \"glew\",\n    \"glm\",\n    \"libpng\",\n    \"nlohmann-json\",\n    \"eigen3\",\n    \"tracy\",\n    \"openxr-loader\"\n  ]\n}\n"
  }
]