├── .clang-format ├── .gitignore ├── CMakeLists.txt ├── LICENSE.md ├── README.md ├── include └── flop │ └── Flop.h ├── lib ├── Buffer.cpp ├── Buffer.hpp ├── CMakeLists.txt ├── ColorMaps.cpp ├── ColorMaps.hpp ├── Flop.cpp ├── FlopContext.hpp ├── Fullscreen.cpp ├── Fullscreen.hpp ├── Image.cpp ├── Image.hpp ├── Kernel.cpp ├── Kernel.hpp ├── STB.cpp ├── VMA.cpp ├── VkGlobals.hpp └── imgui.ini ├── shaders ├── BinToHex.cmake ├── CMakeLists.txt ├── CSFFilter.hlsl ├── ColorCompare.hlsl ├── Common.hlsli ├── ErrorColorMap.hlsl ├── FeatureFilter.hlsl ├── FullscreenVS.hlsl ├── HexToLib.cmake ├── Preview.hlsl ├── Summarize.hlsl ├── Tonemap.hlsl ├── YyCxCz.hlsl ├── flip_kernels.js └── imgui.ini ├── src ├── CMakeLists.txt ├── ImguiVulkan.cpp ├── Main.cpp ├── Preview.cpp ├── Preview.hpp ├── UI.cpp └── UI.hpp └── test ├── CMakeLists.txt ├── Test.cpp ├── flop.png ├── flop_hdr.png ├── flop_ldr.png ├── reference.exr ├── reference.png ├── reference2.png ├── reference3.png ├── test.exr ├── test.png └── test2.png /.clang-format: -------------------------------------------------------------------------------- 1 | AccessModifierOffset: -4 2 | AlignAfterOpenBracket: true 3 | AlignConsecutiveAssignments: true 4 | AlignConsecutiveDeclarations: false 5 | AlignEscapedNewlinesLeft: true 6 | AlignTrailingComments: true 7 | AllowAllParametersOfDeclarationOnNextLine: false 8 | AllowShortBlocksOnASingleLine: false 9 | AllowShortCaseLabelsOnASingleLine: false 10 | AllowShortFunctionsOnASingleLine: false 11 | AllowShortIfStatementsOnASingleLine: false 12 | AllowShortLoopsOnASingleLine: false 13 | AlwaysBreakAfterReturnType: None 14 | AlwaysBreakBeforeMultilineStrings: true 15 | AlwaysBreakTemplateDeclarations: true 16 | BinPackArguments: false 17 | BinPackParameters: false 18 | BreakBeforeBraces: Custom 19 | BraceWrapping: 20 | AfterClass: true 21 | AfterControlStatement: true 22 | AfterEnum: true 23 | AfterFunction: true 24 | AfterNamespace: true 25 | AfterObjCDeclaration: true 26 | AfterStruct: true 27 | AfterUnion: true 28 | AfterExternBlock: true 29 | BeforeCatch: true 30 | BeforeElse: true 31 | IndentBraces: false 32 | SplitEmptyFunction: true 33 | SplitEmptyRecord: true 34 | SplitEmptyNamespace: true 35 | BreakBeforeBinaryOperators: All 36 | BreakBeforeTernaryOperators: true 37 | BreakConstructorInitializers: BeforeComma 38 | BreakStringLiterals: true 39 | ColumnLimit: 80 40 | CommentPragmas: '' 41 | CompactNamespaces: false 42 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 43 | ConstructorInitializerIndentWidth: 4 44 | ContinuationIndentWidth: 4 45 | Cpp11BracedListStyle: true 46 | DerivePointerBinding: false 47 | FixNamespaceComments: true 48 | IndentCaseLabels: false 49 | IndentPPDirectives: AfterHash 50 | IndentWidth: 4 51 | IndentWrappedFunctionNames: false 52 | KeepEmptyLinesAtTheStartOfBlocks: false 53 | Language: Cpp 54 | MaxEmptyLinesToKeep: 1 55 | NamespaceIndentation: Inner 56 | PenaltyBreakBeforeFirstCallParameter: 0 57 | PenaltyBreakComment: 0 58 | PenaltyBreakFirstLessLess: 0 59 | PenaltyBreakString: 1 60 | PenaltyExcessCharacter: 10 61 | PenaltyReturnTypeOnItsOwnLine: 20 62 | PointerAlignment: Left 63 | SortIncludes: true 64 | SortUsingDeclarations: true 65 | SpaceAfterTemplateKeyword: true 66 | SpaceBeforeAssignmentOperators: true 67 | SpaceBeforeParens: ControlStatements 68 | SpaceInEmptyParentheses: false 69 | SpacesBeforeTrailingComments: 1 70 | SpacesInAngles: false 71 | SpacesInCStyleCastParentheses: false 72 | SpacesInContainerLiterals: false 73 | SpacesInParentheses: false 74 | SpacesInSquareBrackets: false 75 | Standard: C++11 76 | TabWidth: 4 77 | UseTab: Never 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build folder 2 | build/ 3 | 4 | # IDE settings 5 | .vscode 6 | .vs 7 | 8 | .clangd 9 | .ccls-cache 10 | .out 11 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | 3 | project(flop) 4 | 5 | option(FLOP_ENABLE_TESTS OFF "Build tests") 6 | option(FLOP_BUILD_EXE ON "Build standalone executable") 7 | option(FLOP_DISABLE_CONSOLE ON "Use the WinMain entry point on windows") 8 | 9 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 10 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 11 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 12 | 13 | add_subdirectory(shaders) 14 | add_subdirectory(lib) 15 | 16 | if(FLOP_BUILD_EXE) 17 | add_subdirectory(src) 18 | endif() 19 | 20 | if(FLOP_ENABLE_TESTS) 21 | enable_testing() 22 | add_subdirectory(test) 23 | endif() 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Jeremy Ong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FLOꟼ 2 | 3 | ![image](https://user-images.githubusercontent.com/250149/156910917-7f61ba21-d8fb-44f0-8ec5-ebef4a64bbc0.png) 4 | 5 | [Prebuilt Single-file Binaries](https://github.com/jeremyong/flop/releases) - currently Windows only 6 | 7 | FLOꟼ (FLOP with a backwards P if rendered correctly) is an MIT-licensed image viewer equipped with a GPU-accelerated perceptual image diffing algorithm based 8 | on [ꟻLIP](https://research.nvidia.com/publication/2020-07_FLIP). Read the accompanying blog post [here](https://www.jeremyong.com/color%20theory/2022/02/19/implementing-the-flip-algorithm/). 9 | 10 | The tool is usable either as a standalone executable for comparing images, or as a library to programmatically 11 | compare images and retrieve a comparison summary and write results to disk. 12 | 13 | *Minor note regarding software build and runtime requirements*: This software was written and tested on one machine, 14 | with a certain set of capabilities afforded to it. Additional testing and possible changes are needed 15 | to verify correct operation on other drivers/platforms/hardware before relaxing requirements. 16 | 17 | https://user-images.githubusercontent.com/250149/154824329-fd790c14-b228-4ca7-b384-3c372e72b09e.mp4 18 | 19 | ## Build prerequisites 20 | 21 | - Relatively modern C++ compiler (some C++20 features are used) 22 | - CMake 3.21+ 23 | - VulkanSDK 1.2.198.1 24 | 25 | ## Build instructions 26 | 27 | ```bash 28 | # Supply a custom generator with -G if you don't want the default one 29 | # Pass -DFLOP_BUILD_EXE=OFF if you don't want to build the standalone executable 30 | # Pass -DFLOP_BUILD_TESTS=ON if you want to compile and run the tests 31 | cmake -B build -S . 32 | cmake --build build 33 | ``` 34 | 35 | The first-time generation time will seem long because several dependencies are being fetched. The 36 | dependencies used are: 37 | 38 | - [GLFW](https://www.glfw.org/) (zlib/libpng) 39 | - [Volk](https://github.com/zeux/volk) (MIT) 40 | - [Dear ImGui](https://github.com/ocornut/imgui) (MIT) 41 | - [DirectXShaderCompiler](https://github.com/microsoft/DirectXShaderCompiler) (LLVM) 42 | - [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) (MIT) 43 | - [CLI11](https://github.com/CLIUtils/CLI11) (BSD 3-clause) 44 | - [stb](https://github.com/nothings/stb) (MIT) 45 | - [tinyexr](https://github.com/syoyo/tinyexr) (BSD 3-clause) 46 | 47 | By default, both the library target and standalone executable are built. If you wish to link the 48 | library against your own code, you may either link the `lflop` library target in cmake, or manually 49 | link the compiled library through some other mechanism. The executable itself is simply `flop.exe` 50 | (or just `flop` on Linux). 51 | 52 | ## Runtime requirements 53 | 54 | - GPU with recent driver 55 | - Windows 10+ 56 | 57 | Linux support should be relatively easy to provide, but is not yet tested. MacOS support will require integration 58 | with the MoltenVK framework. 59 | 60 | ## Usage 61 | 62 | The `flop.exe` can be run without any arguments and is a fully self-contained executable (no installation is required). 63 | When launched in this way, an empty window with a few UI controls to select some images and begin the comparison as seen in the demo above. 64 | 65 | Alternatively, command line options may be passed to optionally run FLOꟼ in headless mode, and supply image paths. 66 | The full help text (obtainable by passing `-h` or `--help`) is reproduced below. 67 | 68 | An image comparison tool and visualizer. All options are optional unless headless mode is requested. 69 | Usage: FLOP [OPTIONS] 70 | 71 | Options: 72 | -h,--help Print this help message and exit 73 | -r,--reference TEXT Path to reference image 74 | -t,--test TEXT Path to test image 75 | -o,--output TEXT Path to output file. 76 | -e,--exposure FLOAT Exposure to apply to an HDR image (log 2 stops) 77 | --tonemapper ENUM:value in {ACES->1,Reinhard->2,Hable->3} OR {1,2,3} 78 | HDR to LDR tonemapping operator 79 | --hl,--headless Request that a gui not be presented 80 | -f,--force Overwrite image if file exists at specified output path 81 | 82 | FLOꟼ may also be used as a library. The `test/` folder demonstrates how to link and programmatically analyze LDR or HDR images. 83 | 84 | ## Differences from the original algorithm 85 | 86 | The original paper assumes fully opaque color values, but it is sometimes useful to compare differences in images that possess an alpha channel. 87 | FLOꟼ accommodates this by detecting the presence of the alpha channel and scaling the Yy component in linearized _Lab_ space prior to the constrast sensitivity filters. 88 | 89 | The HDR FLOꟼ algorithm simply tonemaps both reference and test HDR inputs to LDR ranges with a specified global exposure. 90 | This is done currently for speed (less than 100 ms on my machine), but an local tonemapping operator or exposure bracketing 91 | may be an option in the future. 92 | 93 | ## Limitations 94 | 95 | While FLOꟼ is implemented on the GPU, minimal profiling was actually done to fully optimize it. 96 | Performance varies based on image size. 97 | The various kernel weights themselves are currently hardcoded in the shader bytecode. This may be relaxed in the future. 98 | 99 | The HDR-ꟻLIP algorithm doesn't require specifying exposure when HDR images are supplied. It operates by 100 | automatically determining the exposure range, compute the ꟻLIP error for each exposure, then taking the maximum error per-pixel. 101 | FLOꟼ on the other hand, is meant to be used interactively, so it's recommended that the GUI be used to determine the 102 | appropriate exposure when analyzing HDR images. 103 | 104 | ## References 105 | 106 | - [LDR ꟻLIP](https://research.nvidia.com/publication/2020-07_FLIP) 107 | - [HDR ꟻLIP](https://research.nvidia.com/publication/2021-05_HDR-FLIP) 108 | -------------------------------------------------------------------------------- /include/flop/Flop.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // This interface is designed to be usable by C and C++ runtimes (or any 4 | // language with a C-ABI compatible FFI) 5 | #include 6 | 7 | #ifdef __cplusplus 8 | extern "C" 9 | { 10 | #endif 11 | 12 | struct FlopSummary 13 | { 14 | int width; 15 | int height; 16 | int milliseconds_elapsed; 17 | }; 18 | 19 | // Call to retrieve a C-string describing the last encountered error 20 | char const* flop_get_error(); 21 | 22 | void flop_config_enable_validation(); 23 | 24 | // Prepare the flop runtime for image analysis. 25 | // Returns 0 on success, 1 on failure. 26 | int flop_init(uint32_t instanceExtensionCount, 27 | char const** requiredInstanceExtensions); 28 | 29 | // Compare the left and right LDR images, and write out a summary of the 30 | // analysis 31 | int flop_analyze(char const* image_left_path, 32 | char const* image_right_path, 33 | // The output path is optional, and if not supplied, no 34 | // readback is performed 35 | char const* output_path, 36 | FlopSummary* out_summary); 37 | 38 | int flop_analyze_hdr(char const* image_left_path, 39 | char const* image_right_path, 40 | // The output path is optional, and if not supplied, no 41 | // readback is performed 42 | char const* output_path, 43 | float exposure, 44 | // 0: ACES, 1: Reinhard, 2: Hable 45 | int tonemapper, 46 | FlopSummary* out_summary); 47 | 48 | #ifdef __cplusplus 49 | } // extern "C" 50 | #endif 51 | -------------------------------------------------------------------------------- /lib/Buffer.cpp: -------------------------------------------------------------------------------- 1 | #include "Buffer.hpp" 2 | 3 | using namespace flop; 4 | 5 | uint32_t s_buffer_count; 6 | 7 | Buffer Buffer::create(void const* data, uint32_t size) 8 | { 9 | Buffer buffer; 10 | buffer.size_ = size; 11 | 12 | VkBufferCreateInfo buffer_info{ 13 | .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 14 | .size = size, 15 | .usage 16 | = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, 17 | .sharingMode = VK_SHARING_MODE_EXCLUSIVE, 18 | .queueFamilyIndexCount = 1, 19 | .pQueueFamilyIndices = &g_graphics_queue_index, 20 | }; 21 | 22 | VkBuffer staging; 23 | VmaAllocation staging_allocation; 24 | VmaAllocationCreateInfo allocation_info{.usage = VMA_MEMORY_USAGE_CPU_TO_GPU}; 25 | vmaCreateBuffer(g_allocator, 26 | &buffer_info, 27 | &allocation_info, 28 | &staging, 29 | &staging_allocation, 30 | nullptr); 31 | void* dst; 32 | vmaMapMemory(g_allocator, staging_allocation, &dst); 33 | std::memcpy(dst, data, size); 34 | 35 | allocation_info.usage = VMA_MEMORY_USAGE_GPU_ONLY; 36 | buffer_info.usage 37 | = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; 38 | vmaCreateBuffer(g_allocator, 39 | &buffer_info, 40 | &allocation_info, 41 | &buffer.buffer_, 42 | &buffer.allocation_, 43 | nullptr); 44 | 45 | VkCommandBuffer cb = g_command_buffers[0]; 46 | VkCommandBufferBeginInfo begin{ 47 | .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, 48 | .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; 49 | vkBeginCommandBuffer(cb, &begin); 50 | 51 | VkBufferCopy copy{.srcOffset = 0, .dstOffset = 0, .size = size}; 52 | vkCmdCopyBuffer(cb, staging, buffer.buffer_, 1, ©); 53 | 54 | vkEndCommandBuffer(cb); 55 | VkSubmitInfo submit{ 56 | .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, 57 | .waitSemaphoreCount = 0, 58 | .commandBufferCount = 1, 59 | .pCommandBuffers = &cb, 60 | .signalSemaphoreCount = 0, 61 | }; 62 | vkQueueSubmit(g_graphics_queue, 1, &submit, VK_NULL_HANDLE); 63 | vkQueueWaitIdle(g_graphics_queue); 64 | 65 | vmaUnmapMemory(g_allocator, staging_allocation); 66 | vmaDestroyBuffer(g_allocator, staging, staging_allocation); 67 | 68 | buffer.index_ = s_buffer_count++; 69 | 70 | VkDescriptorBufferInfo descriptor_info{ 71 | .buffer = buffer.buffer_, .offset = 0, .range = size}; 72 | VkWriteDescriptorSet descriptor_write{ 73 | .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, 74 | .dstSet = g_descriptor_set, 75 | .dstBinding = 2, 76 | .dstArrayElement = buffer.index_, 77 | .descriptorCount = 1, 78 | .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 79 | .pBufferInfo = &descriptor_info}; 80 | vkUpdateDescriptorSets(g_device, 1, &descriptor_write, 0, nullptr); 81 | 82 | return buffer; 83 | } 84 | 85 | Buffer Buffer::create(uint32_t size) 86 | { 87 | Buffer buffer; 88 | buffer.size_ = size; 89 | 90 | VkBufferCreateInfo buffer_info{ 91 | .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 92 | .size = size, 93 | .usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, 94 | .sharingMode = VK_SHARING_MODE_EXCLUSIVE, 95 | .queueFamilyIndexCount = 1, 96 | .pQueueFamilyIndices = &g_graphics_queue_index, 97 | }; 98 | 99 | VmaAllocationCreateInfo allocation_info{.usage = VMA_MEMORY_USAGE_GPU_TO_CPU}; 100 | vmaCreateBuffer(g_allocator, 101 | &buffer_info, 102 | &allocation_info, 103 | &buffer.buffer_, 104 | &buffer.allocation_, 105 | nullptr); 106 | 107 | vmaMapMemory(g_allocator, buffer.allocation_, &buffer.data_); 108 | 109 | buffer.index_ = s_buffer_count++; 110 | 111 | VkDescriptorBufferInfo descriptor_info{ 112 | .buffer = buffer.buffer_, .offset = 0, .range = size}; 113 | VkWriteDescriptorSet descriptor_write{ 114 | .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, 115 | .dstSet = g_descriptor_set, 116 | .dstBinding = 2, 117 | .dstArrayElement = buffer.index_, 118 | .descriptorCount = 1, 119 | .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 120 | .pBufferInfo = &descriptor_info}; 121 | vkUpdateDescriptorSets(g_device, 1, &descriptor_write, 0, nullptr); 122 | 123 | return buffer; 124 | } 125 | -------------------------------------------------------------------------------- /lib/Buffer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "VkGlobals.hpp" 4 | 5 | class Buffer 6 | { 7 | public: 8 | // Create a device local buffer and copy supplied data via staging memory 9 | static Buffer create(void const* data, uint32_t size); 10 | 11 | // Create a writable readback buffer 12 | static Buffer create(uint32_t size); 13 | 14 | VkBuffer buffer_ = VK_NULL_HANDLE; 15 | VmaAllocation allocation_ = VK_NULL_HANDLE; 16 | 17 | uint32_t size_ = 0; 18 | uint32_t index_ = 0; 19 | 20 | // Available only for readback data 21 | void* data_ = nullptr; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # External dependencies 2 | include(FetchContent) 3 | 4 | FetchContent_Declare( 5 | stb 6 | GIT_REPOSITORY https://github.com/nothings/stb 7 | GIT_TAG af1a5bc352164740c1cc1354942b1c6b72eacb8a # 2021 09 10 8 | GIT_SHALLOW ON 9 | ) 10 | if(NOT stb_POPULATED) 11 | FetchContent_Populate(stb) 12 | endif() 13 | 14 | if(WIN32) 15 | set(VOLK_STATIC_DEFINES "VK_USE_PLATFORM_WIN32_KHR" CACHE STRING "") 16 | else() 17 | set(VOLK_STATIC_DEFINES "VK_USE_PLATFORM_XLIB_KHR" CACHE STRING "") 18 | endif() 19 | 20 | FetchContent_Declare( 21 | volk 22 | GIT_REPOSITORY https://github.com/zeux/volk 23 | GIT_TAG 1.2.198 24 | GIT_SHALLOW ON 25 | ) 26 | FetchContent_MakeAvailable(volk) 27 | 28 | set(TINYEXR_BUILD_SAMPLE OFF CACHE BOOL "") 29 | FetchContent_Declare( 30 | tinyexr 31 | GIT_REPOSITORY https://github.com/syoyo/tinyexr.git 32 | GIT_TAG v1.0.1 33 | GIT_SHALLOW ON 34 | ) 35 | FetchContent_MakeAvailable(tinyexr) 36 | 37 | set(VMA_STATIC_VULKAN_FUNCTIONS OFF CACHE BOOL "") 38 | set(VMA_DYNAMIC_VULKAN_FUNCTIONS ON CACHE BOOL "") 39 | FetchContent_Declare( 40 | vma 41 | GIT_REPOSITORY https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator 42 | GIT_TAG v3.0.0 43 | GIT_SHALLOW ON 44 | ) 45 | if(NOT vma_POPULATED) 46 | FetchContent_Populate(vma) 47 | endif() 48 | 49 | add_library( 50 | lflop 51 | Buffer.cpp 52 | Buffer.hpp 53 | ColorMaps.cpp 54 | ColorMaps.hpp 55 | Flop.cpp 56 | FlopContext.hpp 57 | Fullscreen.cpp 58 | Fullscreen.hpp 59 | Image.cpp 60 | Image.hpp 61 | Kernel.cpp 62 | Kernel.hpp 63 | STB.cpp 64 | VkGlobals.hpp 65 | VMA.cpp 66 | ) 67 | 68 | target_link_libraries(lflop PUBLIC flop_shaders tinyexr) 69 | 70 | target_compile_features( 71 | lflop 72 | PRIVATE 73 | cxx_std_20 74 | ) 75 | 76 | target_include_directories( 77 | lflop 78 | PUBLIC 79 | ${CMAKE_CURRENT_SOURCE_DIR}/../include 80 | ${vma_SOURCE_DIR}/include 81 | PRIVATE 82 | ${stb_SOURCE_DIR} 83 | ${tinyexr_SOURCE_DIR} 84 | ) 85 | 86 | target_link_libraries( 87 | lflop 88 | PRIVATE 89 | volk 90 | ) 91 | -------------------------------------------------------------------------------- /lib/ColorMaps.cpp: -------------------------------------------------------------------------------- 1 | #include "ColorMaps.hpp" 2 | 3 | #include "Buffer.hpp" 4 | 5 | static Buffer s_magma; 6 | static float const s_magma_data[] 7 | = {0.001462, 0.000466, 0.013866, 0.002258, 0.001295, 0.018331, 0.003279, 8 | 0.002305, 0.023708, 0.004512, 0.003490, 0.029965, 0.005950, 0.004843, 9 | 0.037130, 0.007588, 0.006356, 0.044973, 0.009426, 0.008022, 0.052844, 10 | 0.011465, 0.009828, 0.060750, 0.013708, 0.011771, 0.068667, 0.016156, 11 | 0.013840, 0.076603, 0.018815, 0.016026, 0.084584, 0.021692, 0.018320, 12 | 0.092610, 0.024792, 0.020715, 0.100676, 0.028123, 0.023201, 0.108787, 13 | 0.031696, 0.025765, 0.116965, 0.035520, 0.028397, 0.125209, 0.039608, 14 | 0.031090, 0.133515, 0.043830, 0.033830, 0.141886, 0.048062, 0.036607, 15 | 0.150327, 0.052320, 0.039407, 0.158841, 0.056615, 0.042160, 0.167446, 16 | 0.060949, 0.044794, 0.176129, 0.065330, 0.047318, 0.184892, 0.069764, 17 | 0.049726, 0.193735, 0.074257, 0.052017, 0.202660, 0.078815, 0.054184, 18 | 0.211667, 0.083446, 0.056225, 0.220755, 0.088155, 0.058133, 0.229922, 19 | 0.092949, 0.059904, 0.239164, 0.097833, 0.061531, 0.248477, 0.102815, 20 | 0.063010, 0.257854, 0.107899, 0.064335, 0.267289, 0.113094, 0.065492, 21 | 0.276784, 0.118405, 0.066479, 0.286321, 0.123833, 0.067295, 0.295879, 22 | 0.129380, 0.067935, 0.305443, 0.135053, 0.068391, 0.315000, 0.140858, 23 | 0.068654, 0.324538, 0.146785, 0.068738, 0.334011, 0.152839, 0.068637, 24 | 0.343404, 0.159018, 0.068354, 0.352688, 0.165308, 0.067911, 0.361816, 25 | 0.171713, 0.067305, 0.370771, 0.178212, 0.066576, 0.379497, 0.184801, 26 | 0.065732, 0.387973, 0.191460, 0.064818, 0.396152, 0.198177, 0.063862, 27 | 0.404009, 0.204935, 0.062907, 0.411514, 0.211718, 0.061992, 0.418647, 28 | 0.218512, 0.061158, 0.425392, 0.225302, 0.060445, 0.431742, 0.232077, 29 | 0.059889, 0.437695, 0.238826, 0.059517, 0.443256, 0.245543, 0.059352, 30 | 0.448436, 0.252220, 0.059415, 0.453248, 0.258857, 0.059706, 0.457710, 31 | 0.265447, 0.060237, 0.461840, 0.271994, 0.060994, 0.465660, 0.278493, 32 | 0.061978, 0.469190, 0.284951, 0.063168, 0.472451, 0.291366, 0.064553, 33 | 0.475462, 0.297740, 0.066117, 0.478243, 0.304081, 0.067835, 0.480812, 34 | 0.310382, 0.069702, 0.483186, 0.316654, 0.071690, 0.485380, 0.322899, 35 | 0.073782, 0.487408, 0.329114, 0.075972, 0.489287, 0.335308, 0.078236, 36 | 0.491024, 0.341482, 0.080564, 0.492631, 0.347636, 0.082946, 0.494121, 37 | 0.353773, 0.085373, 0.495501, 0.359898, 0.087831, 0.496778, 0.366012, 38 | 0.090314, 0.497960, 0.372116, 0.092816, 0.499053, 0.378211, 0.095332, 39 | 0.500067, 0.384299, 0.097855, 0.501002, 0.390384, 0.100379, 0.501864, 40 | 0.396467, 0.102902, 0.502658, 0.402548, 0.105420, 0.503386, 0.408629, 41 | 0.107930, 0.504052, 0.414709, 0.110431, 0.504662, 0.420791, 0.112920, 42 | 0.505215, 0.426877, 0.115395, 0.505714, 0.432967, 0.117855, 0.506160, 43 | 0.439062, 0.120298, 0.506555, 0.445163, 0.122724, 0.506901, 0.451271, 44 | 0.125132, 0.507198, 0.457386, 0.127522, 0.507448, 0.463508, 0.129893, 45 | 0.507652, 0.469640, 0.132245, 0.507809, 0.475780, 0.134577, 0.507921, 46 | 0.481929, 0.136891, 0.507989, 0.488088, 0.139186, 0.508011, 0.494258, 47 | 0.141462, 0.507988, 0.500438, 0.143719, 0.507920, 0.506629, 0.145958, 48 | 0.507806, 0.512831, 0.148179, 0.507648, 0.519045, 0.150383, 0.507443, 49 | 0.525270, 0.152569, 0.507192, 0.531507, 0.154739, 0.506895, 0.537755, 50 | 0.156894, 0.506551, 0.544015, 0.159033, 0.506159, 0.550287, 0.161158, 51 | 0.505719, 0.556571, 0.163269, 0.505230, 0.562866, 0.165368, 0.504692, 52 | 0.569172, 0.167454, 0.504105, 0.575490, 0.169530, 0.503466, 0.581819, 53 | 0.171596, 0.502777, 0.588158, 0.173652, 0.502035, 0.594508, 0.175701, 54 | 0.501241, 0.600868, 0.177743, 0.500394, 0.607238, 0.179779, 0.499492, 55 | 0.613617, 0.181811, 0.498536, 0.620005, 0.183840, 0.497524, 0.626401, 56 | 0.185867, 0.496456, 0.632805, 0.187893, 0.495332, 0.639216, 0.189921, 57 | 0.494150, 0.645633, 0.191952, 0.492910, 0.652056, 0.193986, 0.491611, 58 | 0.658483, 0.196027, 0.490253, 0.664915, 0.198075, 0.488836, 0.671349, 59 | 0.200133, 0.487358, 0.677786, 0.202203, 0.485819, 0.684224, 0.204286, 60 | 0.484219, 0.690661, 0.206384, 0.482558, 0.697098, 0.208501, 0.480835, 61 | 0.703532, 0.210638, 0.479049, 0.709962, 0.212797, 0.477201, 0.716387, 62 | 0.214982, 0.475290, 0.722805, 0.217194, 0.473316, 0.729216, 0.219437, 63 | 0.471279, 0.735616, 0.221713, 0.469180, 0.742004, 0.224025, 0.467018, 64 | 0.748378, 0.226377, 0.464794, 0.754737, 0.228772, 0.462509, 0.761077, 65 | 0.231214, 0.460162, 0.767398, 0.233705, 0.457755, 0.773695, 0.236249, 66 | 0.455289, 0.779968, 0.238851, 0.452765, 0.786212, 0.241514, 0.450184, 67 | 0.792427, 0.244242, 0.447543, 0.798608, 0.247040, 0.444848, 0.804752, 68 | 0.249911, 0.442102, 0.810855, 0.252861, 0.439305, 0.816914, 0.255895, 69 | 0.436461, 0.822926, 0.259016, 0.433573, 0.828886, 0.262229, 0.430644, 70 | 0.834791, 0.265540, 0.427671, 0.840636, 0.268953, 0.424666, 0.846416, 71 | 0.272473, 0.421631, 0.852126, 0.276106, 0.418573, 0.857763, 0.279857, 72 | 0.415496, 0.863320, 0.283729, 0.412403, 0.868793, 0.287728, 0.409303, 73 | 0.874176, 0.291859, 0.406205, 0.879464, 0.296125, 0.403118, 0.884651, 74 | 0.300530, 0.400047, 0.889731, 0.305079, 0.397002, 0.894700, 0.309773, 75 | 0.393995, 0.899552, 0.314616, 0.391037, 0.904281, 0.319610, 0.388137, 76 | 0.908884, 0.324755, 0.385308, 0.913354, 0.330052, 0.382563, 0.917689, 77 | 0.335500, 0.379915, 0.921884, 0.341098, 0.377376, 0.925937, 0.346844, 78 | 0.374959, 0.929845, 0.352734, 0.372677, 0.933606, 0.358764, 0.370541, 79 | 0.937221, 0.364929, 0.368567, 0.940687, 0.371224, 0.366762, 0.944006, 80 | 0.377643, 0.365136, 0.947180, 0.384178, 0.363701, 0.950210, 0.390820, 81 | 0.362468, 0.953099, 0.397563, 0.361438, 0.955849, 0.404400, 0.360619, 82 | 0.958464, 0.411324, 0.360014, 0.960949, 0.418323, 0.359630, 0.963310, 83 | 0.425390, 0.359469, 0.965549, 0.432519, 0.359529, 0.967671, 0.439703, 84 | 0.359810, 0.969680, 0.446936, 0.360311, 0.971582, 0.454210, 0.361030, 85 | 0.973381, 0.461520, 0.361965, 0.975082, 0.468861, 0.363111, 0.976690, 86 | 0.476226, 0.364466, 0.978210, 0.483612, 0.366025, 0.979645, 0.491014, 87 | 0.367783, 0.981000, 0.498428, 0.369734, 0.982279, 0.505851, 0.371874, 88 | 0.983485, 0.513280, 0.374198, 0.984622, 0.520713, 0.376698, 0.985693, 89 | 0.528148, 0.379371, 0.986700, 0.535582, 0.382210, 0.987646, 0.543015, 90 | 0.385210, 0.988533, 0.550446, 0.388365, 0.989363, 0.557873, 0.391671, 91 | 0.990138, 0.565296, 0.395122, 0.990871, 0.572706, 0.398714, 0.991558, 92 | 0.580107, 0.402441, 0.992196, 0.587502, 0.406299, 0.992785, 0.594891, 93 | 0.410283, 0.993326, 0.602275, 0.414390, 0.993834, 0.609644, 0.418613, 94 | 0.994309, 0.616999, 0.422950, 0.994738, 0.624350, 0.427397, 0.995122, 95 | 0.631696, 0.431951, 0.995480, 0.639027, 0.436607, 0.995810, 0.646344, 96 | 0.441361, 0.996096, 0.653659, 0.446213, 0.996341, 0.660969, 0.451160, 97 | 0.996580, 0.668256, 0.456192, 0.996775, 0.675541, 0.461314, 0.996925, 98 | 0.682828, 0.466526, 0.997077, 0.690088, 0.471811, 0.997186, 0.697349, 99 | 0.477182, 0.997254, 0.704611, 0.482635, 0.997325, 0.711848, 0.488154, 100 | 0.997351, 0.719089, 0.493755, 0.997351, 0.726324, 0.499428, 0.997341, 101 | 0.733545, 0.505167, 0.997285, 0.740772, 0.510983, 0.997228, 0.747981, 102 | 0.516859, 0.997138, 0.755190, 0.522806, 0.997019, 0.762398, 0.528821, 103 | 0.996898, 0.769591, 0.534892, 0.996727, 0.776795, 0.541039, 0.996571, 104 | 0.783977, 0.547233, 0.996369, 0.791167, 0.553499, 0.996162, 0.798348, 105 | 0.559820, 0.995932, 0.805527, 0.566202, 0.995680, 0.812706, 0.572645, 106 | 0.995424, 0.819875, 0.579140, 0.995131, 0.827052, 0.585701, 0.994851, 107 | 0.834213, 0.592307, 0.994524, 0.841387, 0.598983, 0.994222, 0.848540, 108 | 0.605696, 0.993866, 0.855711, 0.612482, 0.993545, 0.862859, 0.619299, 109 | 0.993170, 0.870024, 0.626189, 0.992831, 0.877168, 0.633109, 0.992440, 110 | 0.884330, 0.640099, 0.992089, 0.891470, 0.647116, 0.991688, 0.898627, 111 | 0.654202, 0.991332, 0.905763, 0.661309, 0.990930, 0.912915, 0.668481, 112 | 0.990570, 0.920049, 0.675675, 0.990175, 0.927196, 0.682926, 0.989815, 113 | 0.934329, 0.690198, 0.989434, 0.941470, 0.697519, 0.989077, 0.948604, 114 | 0.704863, 0.988717, 0.955742, 0.712242, 0.988367, 0.962878, 0.719649, 115 | 0.988033, 0.970012, 0.727077, 0.987691, 0.977154, 0.734536, 0.987387, 116 | 0.984288, 0.742002, 0.987053, 0.991438, 0.749504}; 117 | 118 | static Buffer s_inferno; 119 | static float const s_inferno_data[] 120 | = {0.001462, 0.000466, 0.013866, 0.002267, 0.001270, 0.018570, 0.003299, 121 | 0.002249, 0.024239, 0.004547, 0.003392, 0.030909, 0.006006, 0.004692, 122 | 0.038558, 0.007676, 0.006136, 0.046836, 0.009561, 0.007713, 0.055143, 123 | 0.011663, 0.009417, 0.063460, 0.013995, 0.011225, 0.071862, 0.016561, 124 | 0.013136, 0.080282, 0.019373, 0.015133, 0.088767, 0.022447, 0.017199, 125 | 0.097327, 0.025793, 0.019331, 0.105930, 0.029432, 0.021503, 0.114621, 126 | 0.033385, 0.023702, 0.123397, 0.037668, 0.025921, 0.132232, 0.042253, 127 | 0.028139, 0.141141, 0.046915, 0.030324, 0.150164, 0.051644, 0.032474, 128 | 0.159254, 0.056449, 0.034569, 0.168414, 0.061340, 0.036590, 0.177642, 129 | 0.066331, 0.038504, 0.186962, 0.071429, 0.040294, 0.196354, 0.076637, 130 | 0.041905, 0.205799, 0.081962, 0.043328, 0.215289, 0.087411, 0.044556, 131 | 0.224813, 0.092990, 0.045583, 0.234358, 0.098702, 0.046402, 0.243904, 132 | 0.104551, 0.047008, 0.253430, 0.110536, 0.047399, 0.262912, 0.116656, 133 | 0.047574, 0.272321, 0.122908, 0.047536, 0.281624, 0.129285, 0.047293, 134 | 0.290788, 0.135778, 0.046856, 0.299776, 0.142378, 0.046242, 0.308553, 135 | 0.149073, 0.045468, 0.317085, 0.155850, 0.044559, 0.325338, 0.162689, 136 | 0.043554, 0.333277, 0.169575, 0.042489, 0.340874, 0.176493, 0.041402, 137 | 0.348111, 0.183429, 0.040329, 0.354971, 0.190367, 0.039309, 0.361447, 138 | 0.197297, 0.038400, 0.367535, 0.204209, 0.037632, 0.373238, 0.211095, 139 | 0.037030, 0.378563, 0.217949, 0.036615, 0.383522, 0.224763, 0.036405, 140 | 0.388129, 0.231538, 0.036405, 0.392400, 0.238273, 0.036621, 0.396353, 141 | 0.244967, 0.037055, 0.400007, 0.251620, 0.037705, 0.403378, 0.258234, 142 | 0.038571, 0.406485, 0.264810, 0.039647, 0.409345, 0.271347, 0.040922, 143 | 0.411976, 0.277850, 0.042353, 0.414392, 0.284321, 0.043933, 0.416608, 144 | 0.290763, 0.045644, 0.418637, 0.297178, 0.047470, 0.420491, 0.303568, 145 | 0.049396, 0.422182, 0.309935, 0.051407, 0.423721, 0.316282, 0.053490, 146 | 0.425116, 0.322610, 0.055634, 0.426377, 0.328921, 0.057827, 0.427511, 147 | 0.335217, 0.060060, 0.428524, 0.341500, 0.062325, 0.429425, 0.347771, 148 | 0.064616, 0.430217, 0.354032, 0.066925, 0.430906, 0.360284, 0.069247, 149 | 0.431497, 0.366529, 0.071579, 0.431994, 0.372768, 0.073915, 0.432400, 150 | 0.379001, 0.076253, 0.432719, 0.385228, 0.078591, 0.432955, 0.391453, 151 | 0.080927, 0.433109, 0.397674, 0.083257, 0.433183, 0.403894, 0.085580, 152 | 0.433179, 0.410113, 0.087896, 0.433098, 0.416331, 0.090203, 0.432943, 153 | 0.422549, 0.092501, 0.432714, 0.428768, 0.094790, 0.432412, 0.434987, 154 | 0.097069, 0.432039, 0.441207, 0.099338, 0.431594, 0.447428, 0.101597, 155 | 0.431080, 0.453651, 0.103848, 0.430498, 0.459875, 0.106089, 0.429846, 156 | 0.466100, 0.108322, 0.429125, 0.472328, 0.110547, 0.428334, 0.478558, 157 | 0.112764, 0.427475, 0.484789, 0.114974, 0.426548, 0.491022, 0.117179, 158 | 0.425552, 0.497257, 0.119379, 0.424488, 0.503493, 0.121575, 0.423356, 159 | 0.509730, 0.123769, 0.422156, 0.515967, 0.125960, 0.420887, 0.522206, 160 | 0.128150, 0.419549, 0.528444, 0.130341, 0.418142, 0.534683, 0.132534, 161 | 0.416667, 0.540920, 0.134729, 0.415123, 0.547157, 0.136929, 0.413511, 162 | 0.553392, 0.139134, 0.411829, 0.559624, 0.141346, 0.410078, 0.565854, 163 | 0.143567, 0.408258, 0.572081, 0.145797, 0.406369, 0.578304, 0.148039, 164 | 0.404411, 0.584521, 0.150294, 0.402385, 0.590734, 0.152563, 0.400290, 165 | 0.596940, 0.154848, 0.398125, 0.603139, 0.157151, 0.395891, 0.609330, 166 | 0.159474, 0.393589, 0.615513, 0.161817, 0.391219, 0.621685, 0.164184, 167 | 0.388781, 0.627847, 0.166575, 0.386276, 0.633998, 0.168992, 0.383704, 168 | 0.640135, 0.171438, 0.381065, 0.646260, 0.173914, 0.378359, 0.652369, 169 | 0.176421, 0.375586, 0.658463, 0.178962, 0.372748, 0.664540, 0.181539, 170 | 0.369846, 0.670599, 0.184153, 0.366879, 0.676638, 0.186807, 0.363849, 171 | 0.682656, 0.189501, 0.360757, 0.688653, 0.192239, 0.357603, 0.694627, 172 | 0.195021, 0.354388, 0.700576, 0.197851, 0.351113, 0.706500, 0.200728, 173 | 0.347777, 0.712396, 0.203656, 0.344383, 0.718264, 0.206636, 0.340931, 174 | 0.724103, 0.209670, 0.337424, 0.729909, 0.212759, 0.333861, 0.735683, 175 | 0.215906, 0.330245, 0.741423, 0.219112, 0.326576, 0.747127, 0.222378, 176 | 0.322856, 0.752794, 0.225706, 0.319085, 0.758422, 0.229097, 0.315266, 177 | 0.764010, 0.232554, 0.311399, 0.769556, 0.236077, 0.307485, 0.775059, 178 | 0.239667, 0.303526, 0.780517, 0.243327, 0.299523, 0.785929, 0.247056, 179 | 0.295477, 0.791293, 0.250856, 0.291390, 0.796607, 0.254728, 0.287264, 180 | 0.801871, 0.258674, 0.283099, 0.807082, 0.262692, 0.278898, 0.812239, 181 | 0.266786, 0.274661, 0.817341, 0.270954, 0.270390, 0.822386, 0.275197, 182 | 0.266085, 0.827372, 0.279517, 0.261750, 0.832299, 0.283913, 0.257383, 183 | 0.837165, 0.288385, 0.252988, 0.841969, 0.292933, 0.248564, 0.846709, 184 | 0.297559, 0.244113, 0.851384, 0.302260, 0.239636, 0.855992, 0.307038, 185 | 0.235133, 0.860533, 0.311892, 0.230606, 0.865006, 0.316822, 0.226055, 186 | 0.869409, 0.321827, 0.221482, 0.873741, 0.326906, 0.216886, 0.878001, 187 | 0.332060, 0.212268, 0.882188, 0.337287, 0.207628, 0.886302, 0.342586, 188 | 0.202968, 0.890341, 0.347957, 0.198286, 0.894305, 0.353399, 0.193584, 189 | 0.898192, 0.358911, 0.188860, 0.902003, 0.364492, 0.184116, 0.905735, 190 | 0.370140, 0.179350, 0.909390, 0.375856, 0.174563, 0.912966, 0.381636, 191 | 0.169755, 0.916462, 0.387481, 0.164924, 0.919879, 0.393389, 0.160070, 192 | 0.923215, 0.399359, 0.155193, 0.926470, 0.405389, 0.150292, 0.929644, 193 | 0.411479, 0.145367, 0.932737, 0.417627, 0.140417, 0.935747, 0.423831, 194 | 0.135440, 0.938675, 0.430091, 0.130438, 0.941521, 0.436405, 0.125409, 195 | 0.944285, 0.442772, 0.120354, 0.946965, 0.449191, 0.115272, 0.949562, 196 | 0.455660, 0.110164, 0.952075, 0.462178, 0.105031, 0.954506, 0.468744, 197 | 0.099874, 0.956852, 0.475356, 0.094695, 0.959114, 0.482014, 0.089499, 198 | 0.961293, 0.488716, 0.084289, 0.963387, 0.495462, 0.079073, 0.965397, 199 | 0.502249, 0.073859, 0.967322, 0.509078, 0.068659, 0.969163, 0.515946, 200 | 0.063488, 0.970919, 0.522853, 0.058367, 0.972590, 0.529798, 0.053324, 201 | 0.974176, 0.536780, 0.048392, 0.975677, 0.543798, 0.043618, 0.977092, 202 | 0.550850, 0.039050, 0.978422, 0.557937, 0.034931, 0.979666, 0.565057, 203 | 0.031409, 0.980824, 0.572209, 0.028508, 0.981895, 0.579392, 0.026250, 204 | 0.982881, 0.586606, 0.024661, 0.983779, 0.593849, 0.023770, 0.984591, 205 | 0.601122, 0.023606, 0.985315, 0.608422, 0.024202, 0.985952, 0.615750, 206 | 0.025592, 0.986502, 0.623105, 0.027814, 0.986964, 0.630485, 0.030908, 207 | 0.987337, 0.637890, 0.034916, 0.987622, 0.645320, 0.039886, 0.987819, 208 | 0.652773, 0.045581, 0.987926, 0.660250, 0.051750, 0.987945, 0.667748, 209 | 0.058329, 0.987874, 0.675267, 0.065257, 0.987714, 0.682807, 0.072489, 210 | 0.987464, 0.690366, 0.079990, 0.987124, 0.697944, 0.087731, 0.986694, 211 | 0.705540, 0.095694, 0.986175, 0.713153, 0.103863, 0.985566, 0.720782, 212 | 0.112229, 0.984865, 0.728427, 0.120785, 0.984075, 0.736087, 0.129527, 213 | 0.983196, 0.743758, 0.138453, 0.982228, 0.751442, 0.147565, 0.981173, 214 | 0.759135, 0.156863, 0.980032, 0.766837, 0.166353, 0.978806, 0.774545, 215 | 0.176037, 0.977497, 0.782258, 0.185923, 0.976108, 0.789974, 0.196018, 216 | 0.974638, 0.797692, 0.206332, 0.973088, 0.805409, 0.216877, 0.971468, 217 | 0.813122, 0.227658, 0.969783, 0.820825, 0.238686, 0.968041, 0.828515, 218 | 0.249972, 0.966243, 0.836191, 0.261534, 0.964394, 0.843848, 0.273391, 219 | 0.962517, 0.851476, 0.285546, 0.960626, 0.859069, 0.298010, 0.958720, 220 | 0.866624, 0.310820, 0.956834, 0.874129, 0.323974, 0.954997, 0.881569, 221 | 0.337475, 0.953215, 0.888942, 0.351369, 0.951546, 0.896226, 0.365627, 222 | 0.950018, 0.903409, 0.380271, 0.948683, 0.910473, 0.395289, 0.947594, 223 | 0.917399, 0.410665, 0.946809, 0.924168, 0.426373, 0.946392, 0.930761, 224 | 0.442367, 0.946403, 0.937159, 0.458592, 0.946903, 0.943348, 0.474970, 225 | 0.947937, 0.949318, 0.491426, 0.949545, 0.955063, 0.507860, 0.951740, 226 | 0.960587, 0.524203, 0.954529, 0.965896, 0.540361, 0.957896, 0.971003, 227 | 0.556275, 0.961812, 0.975924, 0.571925, 0.966249, 0.980678, 0.587206, 228 | 0.971162, 0.985282, 0.602154, 0.976511, 0.989753, 0.616760, 0.982257, 229 | 0.994109, 0.631017, 0.988362, 0.998364, 0.644924}; 230 | 231 | static Buffer s_plasma; 232 | static float const s_plasma_data[] 233 | = {0.050383, 0.029803, 0.527975, 0.063536, 0.028426, 0.533124, 0.075353, 234 | 0.027206, 0.538007, 0.086222, 0.026125, 0.542658, 0.096379, 0.025165, 235 | 0.547103, 0.105980, 0.024309, 0.551368, 0.115124, 0.023556, 0.555468, 236 | 0.123903, 0.022878, 0.559423, 0.132381, 0.022258, 0.563250, 0.140603, 237 | 0.021687, 0.566959, 0.148607, 0.021154, 0.570562, 0.156421, 0.020651, 238 | 0.574065, 0.164070, 0.020171, 0.577478, 0.171574, 0.019706, 0.580806, 239 | 0.178950, 0.019252, 0.584054, 0.186213, 0.018803, 0.587228, 0.193374, 240 | 0.018354, 0.590330, 0.200445, 0.017902, 0.593364, 0.207435, 0.017442, 241 | 0.596333, 0.214350, 0.016973, 0.599239, 0.221197, 0.016497, 0.602083, 242 | 0.227983, 0.016007, 0.604867, 0.234715, 0.015502, 0.607592, 0.241396, 243 | 0.014979, 0.610259, 0.248032, 0.014439, 0.612868, 0.254627, 0.013882, 244 | 0.615419, 0.261183, 0.013308, 0.617911, 0.267703, 0.012716, 0.620346, 245 | 0.274191, 0.012109, 0.622722, 0.280648, 0.011488, 0.625038, 0.287076, 246 | 0.010855, 0.627295, 0.293478, 0.010213, 0.629490, 0.299855, 0.009561, 247 | 0.631624, 0.306210, 0.008902, 0.633694, 0.312543, 0.008239, 0.635700, 248 | 0.318856, 0.007576, 0.637640, 0.325150, 0.006915, 0.639512, 0.331426, 249 | 0.006261, 0.641316, 0.337683, 0.005618, 0.643049, 0.343925, 0.004991, 250 | 0.644710, 0.350150, 0.004382, 0.646298, 0.356359, 0.003798, 0.647810, 251 | 0.362553, 0.003243, 0.649245, 0.368733, 0.002724, 0.650601, 0.374897, 252 | 0.002245, 0.651876, 0.381047, 0.001814, 0.653068, 0.387183, 0.001434, 253 | 0.654177, 0.393304, 0.001114, 0.655199, 0.399411, 0.000859, 0.656133, 254 | 0.405503, 0.000678, 0.656977, 0.411580, 0.000577, 0.657730, 0.417642, 255 | 0.000564, 0.658390, 0.423689, 0.000646, 0.658956, 0.429719, 0.000831, 256 | 0.659425, 0.435734, 0.001127, 0.659797, 0.441732, 0.001540, 0.660069, 257 | 0.447714, 0.002080, 0.660240, 0.453677, 0.002755, 0.660310, 0.459623, 258 | 0.003574, 0.660277, 0.465550, 0.004545, 0.660139, 0.471457, 0.005678, 259 | 0.659897, 0.477344, 0.006980, 0.659549, 0.483210, 0.008460, 0.659095, 260 | 0.489055, 0.010127, 0.658534, 0.494877, 0.011990, 0.657865, 0.500678, 261 | 0.014055, 0.657088, 0.506454, 0.016333, 0.656202, 0.512206, 0.018833, 262 | 0.655209, 0.517933, 0.021563, 0.654109, 0.523633, 0.024532, 0.652901, 263 | 0.529306, 0.027747, 0.651586, 0.534952, 0.031217, 0.650165, 0.540570, 264 | 0.034950, 0.648640, 0.546157, 0.038954, 0.647010, 0.551715, 0.043136, 265 | 0.645277, 0.557243, 0.047331, 0.643443, 0.562738, 0.051545, 0.641509, 266 | 0.568201, 0.055778, 0.639477, 0.573632, 0.060028, 0.637349, 0.579029, 267 | 0.064296, 0.635126, 0.584391, 0.068579, 0.632812, 0.589719, 0.072878, 268 | 0.630408, 0.595011, 0.077190, 0.627917, 0.600266, 0.081516, 0.625342, 269 | 0.605485, 0.085854, 0.622686, 0.610667, 0.090204, 0.619951, 0.615812, 270 | 0.094564, 0.617140, 0.620919, 0.098934, 0.614257, 0.625987, 0.103312, 271 | 0.611305, 0.631017, 0.107699, 0.608287, 0.636008, 0.112092, 0.605205, 272 | 0.640959, 0.116492, 0.602065, 0.645872, 0.120898, 0.598867, 0.650746, 273 | 0.125309, 0.595617, 0.655580, 0.129725, 0.592317, 0.660374, 0.134144, 274 | 0.588971, 0.665129, 0.138566, 0.585582, 0.669845, 0.142992, 0.582154, 275 | 0.674522, 0.147419, 0.578688, 0.679160, 0.151848, 0.575189, 0.683758, 276 | 0.156278, 0.571660, 0.688318, 0.160709, 0.568103, 0.692840, 0.165141, 277 | 0.564522, 0.697324, 0.169573, 0.560919, 0.701769, 0.174005, 0.557296, 278 | 0.706178, 0.178437, 0.553657, 0.710549, 0.182868, 0.550004, 0.714883, 279 | 0.187299, 0.546338, 0.719181, 0.191729, 0.542663, 0.723444, 0.196158, 280 | 0.538981, 0.727670, 0.200586, 0.535293, 0.731862, 0.205013, 0.531601, 281 | 0.736019, 0.209439, 0.527908, 0.740143, 0.213864, 0.524216, 0.744232, 282 | 0.218288, 0.520524, 0.748289, 0.222711, 0.516834, 0.752312, 0.227133, 283 | 0.513149, 0.756304, 0.231555, 0.509468, 0.760264, 0.235976, 0.505794, 284 | 0.764193, 0.240396, 0.502126, 0.768090, 0.244817, 0.498465, 0.771958, 285 | 0.249237, 0.494813, 0.775796, 0.253658, 0.491171, 0.779604, 0.258078, 286 | 0.487539, 0.783383, 0.262500, 0.483918, 0.787133, 0.266922, 0.480307, 287 | 0.790855, 0.271345, 0.476706, 0.794549, 0.275770, 0.473117, 0.798216, 288 | 0.280197, 0.469538, 0.801855, 0.284626, 0.465971, 0.805467, 0.289057, 289 | 0.462415, 0.809052, 0.293491, 0.458870, 0.812612, 0.297928, 0.455338, 290 | 0.816144, 0.302368, 0.451816, 0.819651, 0.306812, 0.448306, 0.823132, 291 | 0.311261, 0.444806, 0.826588, 0.315714, 0.441316, 0.830018, 0.320172, 292 | 0.437836, 0.833422, 0.324635, 0.434366, 0.836801, 0.329105, 0.430905, 293 | 0.840155, 0.333580, 0.427455, 0.843484, 0.338062, 0.424013, 0.846788, 294 | 0.342551, 0.420579, 0.850066, 0.347048, 0.417153, 0.853319, 0.351553, 295 | 0.413734, 0.856547, 0.356066, 0.410322, 0.859750, 0.360588, 0.406917, 296 | 0.862927, 0.365119, 0.403519, 0.866078, 0.369660, 0.400126, 0.869203, 297 | 0.374212, 0.396738, 0.872303, 0.378774, 0.393355, 0.875376, 0.383347, 298 | 0.389976, 0.878423, 0.387932, 0.386600, 0.881443, 0.392529, 0.383229, 299 | 0.884436, 0.397139, 0.379860, 0.887402, 0.401762, 0.376494, 0.890340, 300 | 0.406398, 0.373130, 0.893250, 0.411048, 0.369768, 0.896131, 0.415712, 301 | 0.366407, 0.898984, 0.420392, 0.363047, 0.901807, 0.425087, 0.359688, 302 | 0.904601, 0.429797, 0.356329, 0.907365, 0.434524, 0.352970, 0.910098, 303 | 0.439268, 0.349610, 0.912800, 0.444029, 0.346251, 0.915471, 0.448807, 304 | 0.342890, 0.918109, 0.453603, 0.339529, 0.920714, 0.458417, 0.336166, 305 | 0.923287, 0.463251, 0.332801, 0.925825, 0.468103, 0.329435, 0.928329, 306 | 0.472975, 0.326067, 0.930798, 0.477867, 0.322697, 0.933232, 0.482780, 307 | 0.319325, 0.935630, 0.487712, 0.315952, 0.937990, 0.492667, 0.312575, 308 | 0.940313, 0.497642, 0.309197, 0.942598, 0.502639, 0.305816, 0.944844, 309 | 0.507658, 0.302433, 0.947051, 0.512699, 0.299049, 0.949217, 0.517763, 310 | 0.295662, 0.951344, 0.522850, 0.292275, 0.953428, 0.527960, 0.288883, 311 | 0.955470, 0.533093, 0.285490, 0.957469, 0.538250, 0.282096, 0.959424, 312 | 0.543431, 0.278701, 0.961336, 0.548636, 0.275305, 0.963203, 0.553865, 313 | 0.271909, 0.965024, 0.559118, 0.268513, 0.966798, 0.564396, 0.265118, 314 | 0.968526, 0.569700, 0.261721, 0.970205, 0.575028, 0.258325, 0.971835, 315 | 0.580382, 0.254931, 0.973416, 0.585761, 0.251540, 0.974947, 0.591165, 316 | 0.248151, 0.976428, 0.596595, 0.244767, 0.977856, 0.602051, 0.241387, 317 | 0.979233, 0.607532, 0.238013, 0.980556, 0.613039, 0.234646, 0.981826, 318 | 0.618572, 0.231287, 0.983041, 0.624131, 0.227937, 0.984199, 0.629718, 319 | 0.224595, 0.985301, 0.635330, 0.221265, 0.986345, 0.640969, 0.217948, 320 | 0.987332, 0.646633, 0.214648, 0.988260, 0.652325, 0.211364, 0.989128, 321 | 0.658043, 0.208100, 0.989935, 0.663787, 0.204859, 0.990681, 0.669558, 322 | 0.201642, 0.991365, 0.675355, 0.198453, 0.991985, 0.681179, 0.195295, 323 | 0.992541, 0.687030, 0.192170, 0.993032, 0.692907, 0.189084, 0.993456, 324 | 0.698810, 0.186041, 0.993814, 0.704741, 0.183043, 0.994103, 0.710698, 325 | 0.180097, 0.994324, 0.716681, 0.177208, 0.994474, 0.722691, 0.174381, 326 | 0.994553, 0.728728, 0.171622, 0.994561, 0.734791, 0.168938, 0.994495, 327 | 0.740880, 0.166335, 0.994355, 0.746995, 0.163821, 0.994141, 0.753137, 328 | 0.161404, 0.993851, 0.759304, 0.159092, 0.993482, 0.765499, 0.156891, 329 | 0.993033, 0.771720, 0.154808, 0.992505, 0.777967, 0.152855, 0.991897, 330 | 0.784239, 0.151042, 0.991209, 0.790537, 0.149377, 0.990439, 0.796859, 331 | 0.147870, 0.989587, 0.803205, 0.146529, 0.988648, 0.809579, 0.145357, 332 | 0.987621, 0.815978, 0.144363, 0.986509, 0.822401, 0.143557, 0.985314, 333 | 0.828846, 0.142945, 0.984031, 0.835315, 0.142528, 0.982653, 0.841812, 334 | 0.142303, 0.981190, 0.848329, 0.142279, 0.979644, 0.854866, 0.142453, 335 | 0.977995, 0.861432, 0.142808, 0.976265, 0.868016, 0.143351, 0.974443, 336 | 0.874622, 0.144061, 0.972530, 0.881250, 0.144923, 0.970533, 0.887896, 337 | 0.145919, 0.968443, 0.894564, 0.147014, 0.966271, 0.901249, 0.148180, 338 | 0.964021, 0.907950, 0.149370, 0.961681, 0.914672, 0.150520, 0.959276, 339 | 0.921407, 0.151566, 0.956808, 0.928152, 0.152409, 0.954287, 0.934908, 340 | 0.152921, 0.951726, 0.941671, 0.152925, 0.949151, 0.948435, 0.152178, 341 | 0.946602, 0.955190, 0.150328, 0.944152, 0.961916, 0.146861, 0.941896, 342 | 0.968590, 0.140956, 0.940015, 0.975158, 0.131326}; 343 | 344 | static Buffer s_viridis; 345 | static float const s_viridis_data[] 346 | = {0.267004, 0.004874, 0.329415, 0.268510, 0.009605, 0.335427, 0.269944, 347 | 0.014625, 0.341379, 0.271305, 0.019942, 0.347269, 0.272594, 0.025563, 348 | 0.353093, 0.273809, 0.031497, 0.358853, 0.274952, 0.037752, 0.364543, 349 | 0.276022, 0.044167, 0.370164, 0.277018, 0.050344, 0.375715, 0.277941, 350 | 0.056324, 0.381191, 0.278791, 0.062145, 0.386592, 0.279566, 0.067836, 351 | 0.391917, 0.280267, 0.073417, 0.397163, 0.280894, 0.078907, 0.402329, 352 | 0.281446, 0.084320, 0.407414, 0.281924, 0.089666, 0.412415, 0.282327, 353 | 0.094955, 0.417331, 0.282656, 0.100196, 0.422160, 0.282910, 0.105393, 354 | 0.426902, 0.283091, 0.110553, 0.431554, 0.283197, 0.115680, 0.436115, 355 | 0.283229, 0.120777, 0.440584, 0.283187, 0.125848, 0.444960, 0.283072, 356 | 0.130895, 0.449241, 0.282884, 0.135920, 0.453427, 0.282623, 0.140926, 357 | 0.457517, 0.282290, 0.145912, 0.461510, 0.281887, 0.150881, 0.465405, 358 | 0.281412, 0.155834, 0.469201, 0.280868, 0.160771, 0.472899, 0.280255, 359 | 0.165693, 0.476498, 0.279574, 0.170599, 0.479997, 0.278826, 0.175490, 360 | 0.483397, 0.278012, 0.180367, 0.486697, 0.277134, 0.185228, 0.489898, 361 | 0.276194, 0.190074, 0.493001, 0.275191, 0.194905, 0.496005, 0.274128, 362 | 0.199721, 0.498911, 0.273006, 0.204520, 0.501721, 0.271828, 0.209303, 363 | 0.504434, 0.270595, 0.214069, 0.507052, 0.269308, 0.218818, 0.509577, 364 | 0.267968, 0.223549, 0.512008, 0.266580, 0.228262, 0.514349, 0.265145, 365 | 0.232956, 0.516599, 0.263663, 0.237631, 0.518762, 0.262138, 0.242286, 366 | 0.520837, 0.260571, 0.246922, 0.522828, 0.258965, 0.251537, 0.524736, 367 | 0.257322, 0.256130, 0.526563, 0.255645, 0.260703, 0.528312, 0.253935, 368 | 0.265254, 0.529983, 0.252194, 0.269783, 0.531579, 0.250425, 0.274290, 369 | 0.533103, 0.248629, 0.278775, 0.534556, 0.246811, 0.283237, 0.535941, 370 | 0.244972, 0.287675, 0.537260, 0.243113, 0.292092, 0.538516, 0.241237, 371 | 0.296485, 0.539709, 0.239346, 0.300855, 0.540844, 0.237441, 0.305202, 372 | 0.541921, 0.235526, 0.309527, 0.542944, 0.233603, 0.313828, 0.543914, 373 | 0.231674, 0.318106, 0.544834, 0.229739, 0.322361, 0.545706, 0.227802, 374 | 0.326594, 0.546532, 0.225863, 0.330805, 0.547314, 0.223925, 0.334994, 375 | 0.548053, 0.221989, 0.339161, 0.548752, 0.220057, 0.343307, 0.549413, 376 | 0.218130, 0.347432, 0.550038, 0.216210, 0.351535, 0.550627, 0.214298, 377 | 0.355619, 0.551184, 0.212395, 0.359683, 0.551710, 0.210503, 0.363727, 378 | 0.552206, 0.208623, 0.367752, 0.552675, 0.206756, 0.371758, 0.553117, 379 | 0.204903, 0.375746, 0.553533, 0.203063, 0.379716, 0.553925, 0.201239, 380 | 0.383670, 0.554294, 0.199430, 0.387607, 0.554642, 0.197636, 0.391528, 381 | 0.554969, 0.195860, 0.395433, 0.555276, 0.194100, 0.399323, 0.555565, 382 | 0.192357, 0.403199, 0.555836, 0.190631, 0.407061, 0.556089, 0.188923, 383 | 0.410910, 0.556326, 0.187231, 0.414746, 0.556547, 0.185556, 0.418570, 384 | 0.556753, 0.183898, 0.422383, 0.556944, 0.182256, 0.426184, 0.557120, 385 | 0.180629, 0.429975, 0.557282, 0.179019, 0.433756, 0.557430, 0.177423, 386 | 0.437527, 0.557565, 0.175841, 0.441290, 0.557685, 0.174274, 0.445044, 387 | 0.557792, 0.172719, 0.448791, 0.557885, 0.171176, 0.452530, 0.557965, 388 | 0.169646, 0.456262, 0.558030, 0.168126, 0.459988, 0.558082, 0.166617, 389 | 0.463708, 0.558119, 0.165117, 0.467423, 0.558141, 0.163625, 0.471133, 390 | 0.558148, 0.162142, 0.474838, 0.558140, 0.160665, 0.478540, 0.558115, 391 | 0.159194, 0.482237, 0.558073, 0.157729, 0.485932, 0.558013, 0.156270, 392 | 0.489624, 0.557936, 0.154815, 0.493313, 0.557840, 0.153364, 0.497000, 393 | 0.557724, 0.151918, 0.500685, 0.557587, 0.150476, 0.504369, 0.557430, 394 | 0.149039, 0.508051, 0.557250, 0.147607, 0.511733, 0.557049, 0.146180, 395 | 0.515413, 0.556823, 0.144759, 0.519093, 0.556572, 0.143343, 0.522773, 396 | 0.556295, 0.141935, 0.526453, 0.555991, 0.140536, 0.530132, 0.555659, 397 | 0.139147, 0.533812, 0.555298, 0.137770, 0.537492, 0.554906, 0.136408, 398 | 0.541173, 0.554483, 0.135066, 0.544853, 0.554029, 0.133743, 0.548535, 399 | 0.553541, 0.132444, 0.552216, 0.553018, 0.131172, 0.555899, 0.552459, 400 | 0.129933, 0.559582, 0.551864, 0.128729, 0.563265, 0.551229, 0.127568, 401 | 0.566949, 0.550556, 0.126453, 0.570633, 0.549841, 0.125394, 0.574318, 402 | 0.549086, 0.124395, 0.578002, 0.548287, 0.123463, 0.581687, 0.547445, 403 | 0.122606, 0.585371, 0.546557, 0.121831, 0.589055, 0.545623, 0.121148, 404 | 0.592739, 0.544641, 0.120565, 0.596422, 0.543611, 0.120092, 0.600104, 405 | 0.542530, 0.119738, 0.603785, 0.541400, 0.119512, 0.607464, 0.540218, 406 | 0.119423, 0.611141, 0.538982, 0.119483, 0.614817, 0.537692, 0.119699, 407 | 0.618490, 0.536347, 0.120081, 0.622161, 0.534946, 0.120638, 0.625828, 408 | 0.533488, 0.121380, 0.629492, 0.531973, 0.122312, 0.633153, 0.530398, 409 | 0.123444, 0.636809, 0.528763, 0.124780, 0.640461, 0.527068, 0.126326, 410 | 0.644107, 0.525311, 0.128087, 0.647749, 0.523491, 0.130067, 0.651384, 411 | 0.521608, 0.132268, 0.655014, 0.519661, 0.134692, 0.658636, 0.517649, 412 | 0.137339, 0.662252, 0.515571, 0.140210, 0.665859, 0.513427, 0.143303, 413 | 0.669459, 0.511215, 0.146616, 0.673050, 0.508936, 0.150148, 0.676631, 414 | 0.506589, 0.153894, 0.680203, 0.504172, 0.157851, 0.683765, 0.501686, 415 | 0.162016, 0.687316, 0.499129, 0.166383, 0.690856, 0.496502, 0.170948, 416 | 0.694384, 0.493803, 0.175707, 0.697900, 0.491033, 0.180653, 0.701402, 417 | 0.488189, 0.185783, 0.704891, 0.485273, 0.191090, 0.708366, 0.482284, 418 | 0.196571, 0.711827, 0.479221, 0.202219, 0.715272, 0.476084, 0.208030, 419 | 0.718701, 0.472873, 0.214000, 0.722114, 0.469588, 0.220124, 0.725509, 420 | 0.466226, 0.226397, 0.728888, 0.462789, 0.232815, 0.732247, 0.459277, 421 | 0.239374, 0.735588, 0.455688, 0.246070, 0.738910, 0.452024, 0.252899, 422 | 0.742211, 0.448284, 0.259857, 0.745492, 0.444467, 0.266941, 0.748751, 423 | 0.440573, 0.274149, 0.751988, 0.436601, 0.281477, 0.755203, 0.432552, 424 | 0.288921, 0.758394, 0.428426, 0.296479, 0.761561, 0.424223, 0.304148, 425 | 0.764704, 0.419943, 0.311925, 0.767822, 0.415586, 0.319809, 0.770914, 426 | 0.411152, 0.327796, 0.773980, 0.406640, 0.335885, 0.777018, 0.402049, 427 | 0.344074, 0.780029, 0.397381, 0.352360, 0.783011, 0.392636, 0.360741, 428 | 0.785964, 0.387814, 0.369214, 0.788888, 0.382914, 0.377779, 0.791781, 429 | 0.377939, 0.386433, 0.794644, 0.372886, 0.395174, 0.797475, 0.367757, 430 | 0.404001, 0.800275, 0.362552, 0.412913, 0.803041, 0.357269, 0.421908, 431 | 0.805774, 0.351910, 0.430983, 0.808473, 0.346476, 0.440137, 0.811138, 432 | 0.340967, 0.449368, 0.813768, 0.335384, 0.458674, 0.816363, 0.329727, 433 | 0.468053, 0.818921, 0.323998, 0.477504, 0.821444, 0.318195, 0.487026, 434 | 0.823929, 0.312321, 0.496615, 0.826376, 0.306377, 0.506271, 0.828786, 435 | 0.300362, 0.515992, 0.831158, 0.294279, 0.525776, 0.833491, 0.288127, 436 | 0.535621, 0.835785, 0.281908, 0.545524, 0.838039, 0.275626, 0.555484, 437 | 0.840254, 0.269281, 0.565498, 0.842430, 0.262877, 0.575563, 0.844566, 438 | 0.256415, 0.585678, 0.846661, 0.249897, 0.595839, 0.848717, 0.243329, 439 | 0.606045, 0.850733, 0.236712, 0.616293, 0.852709, 0.230052, 0.626579, 440 | 0.854645, 0.223353, 0.636902, 0.856542, 0.216620, 0.647257, 0.858400, 441 | 0.209861, 0.657642, 0.860219, 0.203082, 0.668054, 0.861999, 0.196293, 442 | 0.678489, 0.863742, 0.189503, 0.688944, 0.865448, 0.182725, 0.699415, 443 | 0.867117, 0.175971, 0.709898, 0.868751, 0.169257, 0.720391, 0.870350, 444 | 0.162603, 0.730889, 0.871916, 0.156029, 0.741388, 0.873449, 0.149561, 445 | 0.751884, 0.874951, 0.143228, 0.762373, 0.876424, 0.137064, 0.772852, 446 | 0.877868, 0.131109, 0.783315, 0.879285, 0.125405, 0.793760, 0.880678, 447 | 0.120005, 0.804182, 0.882046, 0.114965, 0.814576, 0.883393, 0.110347, 448 | 0.824940, 0.884720, 0.106217, 0.835270, 0.886029, 0.102646, 0.845561, 449 | 0.887322, 0.099702, 0.855810, 0.888601, 0.097452, 0.866013, 0.889868, 450 | 0.095953, 0.876168, 0.891125, 0.095250, 0.886271, 0.892374, 0.095374, 451 | 0.896320, 0.893616, 0.096335, 0.906311, 0.894855, 0.098125, 0.916242, 452 | 0.896091, 0.100717, 0.926106, 0.897330, 0.104071, 0.935904, 0.898570, 453 | 0.108131, 0.945636, 0.899815, 0.112838, 0.955300, 0.901065, 0.118128, 454 | 0.964894, 0.902323, 0.123941, 0.974417, 0.903590, 0.130215, 0.983868, 455 | 0.904867, 0.136897, 0.993248, 0.906157, 0.143936}; 456 | 457 | void upload_color_maps() 458 | { 459 | s_magma = Buffer::create( 460 | reinterpret_cast(s_magma_data), sizeof(s_magma_data)); 461 | s_inferno = Buffer::create( 462 | reinterpret_cast(s_inferno_data), sizeof(s_inferno_data)); 463 | s_plasma = Buffer::create( 464 | reinterpret_cast(s_plasma_data), sizeof(s_plasma_data)); 465 | s_viridis = Buffer::create( 466 | reinterpret_cast(s_viridis_data), sizeof(s_viridis_data)); 467 | } 468 | 469 | Buffer& get_color_map(ColorMap color_map) 470 | { 471 | switch (color_map) 472 | { 473 | using enum ColorMap; 474 | case Magma: 475 | return s_magma; 476 | case Inferno: 477 | return s_inferno; 478 | case Plasma: 479 | return s_plasma; 480 | case Viridis: 481 | return s_viridis; 482 | default: 483 | break; 484 | } 485 | return s_viridis; 486 | } 487 | -------------------------------------------------------------------------------- /lib/ColorMaps.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | enum class ColorMap 4 | { 5 | Viridis, 6 | Inferno, 7 | Magma, 8 | Plasma, 9 | }; 10 | 11 | void upload_color_maps(); 12 | 13 | class Buffer; 14 | Buffer& get_color_map(ColorMap color_map); 15 | -------------------------------------------------------------------------------- /lib/FlopContext.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Buffer.hpp" 4 | #include "Image.hpp" 5 | #include "Kernel.hpp" 6 | #include "Fullscreen.hpp" 7 | 8 | namespace flop 9 | { 10 | struct ImagePacket 11 | { 12 | // Pipeline: 13 | // The source_ image is first converted to linearized CIELAB space (YyCxCz) 14 | // in yycxcz_. The yycxcz_ image is then blurred in the x direction into 15 | // yycxcz_blur_x. That result is then blurred in the y direction back into 16 | // yycxcz_. 17 | Image source_; 18 | Image yycxcz_; 19 | Image yycxcz_blur_x_; 20 | Image yycxcz_blurred_; 21 | Image feature_blur_x_; 22 | }; 23 | 24 | inline ImagePacket g_reference; 25 | inline ImagePacket g_test; 26 | inline Image g_error; 27 | inline Image g_error_color; 28 | inline Image g_error_readback; 29 | inline Buffer g_error_histogram; 30 | inline Kernel g_csf_filter_x; 31 | inline Kernel g_csf_filter_y; 32 | inline Kernel g_color_compare; 33 | inline Kernel g_feature_filter_x; 34 | inline Kernel g_feature_filter_y; 35 | inline Kernel g_summarize; 36 | inline Fullscreen g_yycxcz; 37 | inline Fullscreen g_error_color_map; 38 | } // namespace flop 39 | -------------------------------------------------------------------------------- /lib/Fullscreen.cpp: -------------------------------------------------------------------------------- 1 | #include "Fullscreen.hpp" 2 | 3 | #include "Image.hpp" 4 | #include "Kernel.hpp" 5 | 6 | #include 7 | 8 | using namespace flop; 9 | 10 | static VkShaderModule s_vs; 11 | 12 | void Fullscreen::init(uint8_t const* shader_bytecode, 13 | size_t bytecode_size, 14 | uint8_t pushconstant_size) 15 | { 16 | VkPushConstantRange push_constant_range{ 17 | .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 18 | .offset = 0, 19 | .size = pushconstant_size}; 20 | VkPipelineLayoutCreateInfo layout_info{ 21 | .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, 22 | .setLayoutCount = 1, 23 | .pSetLayouts = &g_descriptor_set_layout, 24 | .pushConstantRangeCount = 1, 25 | .pPushConstantRanges = &push_constant_range}; 26 | vkCreatePipelineLayout(g_device, &layout_info, nullptr, &layout_); 27 | 28 | pushconstant_size_ = pushconstant_size; 29 | 30 | if (s_vs == VK_NULL_HANDLE) 31 | { 32 | s_vs = Kernel::compile_shader( 33 | FullscreenVS_spv_data, FullscreenVS_spv_size); 34 | } 35 | VkShaderModule ps_shader 36 | = Kernel::compile_shader(shader_bytecode, bytecode_size); 37 | 38 | VkPipelineShaderStageCreateInfo stages[2] = { 39 | { 40 | .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, 41 | .stage = VK_SHADER_STAGE_VERTEX_BIT, 42 | .module = s_vs, 43 | .pName = "VSMain", 44 | }, 45 | { 46 | .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, 47 | .stage = VK_SHADER_STAGE_FRAGMENT_BIT, 48 | .module = ps_shader, 49 | .pName = "PSMain", 50 | }, 51 | }; 52 | 53 | VkPipelineVertexInputStateCreateInfo vi{ 54 | .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, 55 | .vertexBindingDescriptionCount = 0, 56 | .pVertexBindingDescriptions = nullptr, 57 | .vertexAttributeDescriptionCount = 0, 58 | .pVertexAttributeDescriptions = nullptr, 59 | }; 60 | 61 | VkPipelineInputAssemblyStateCreateInfo ia{ 62 | .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, 63 | .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}; 64 | VkPipelineViewportStateCreateInfo v{ 65 | .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, 66 | .viewportCount = 1, 67 | .pViewports = nullptr, 68 | .scissorCount = 1, 69 | .pScissors = nullptr}; 70 | VkPipelineRasterizationStateCreateInfo rs{ 71 | .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, 72 | .depthClampEnable = false, 73 | .rasterizerDiscardEnable = false, 74 | .polygonMode = VK_POLYGON_MODE_FILL, 75 | .cullMode = VK_CULL_MODE_NONE, 76 | .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, 77 | .lineWidth = 1.f}; 78 | VkPipelineMultisampleStateCreateInfo ms{ 79 | .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, 80 | .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, 81 | .sampleShadingEnable = VK_FALSE, 82 | .pSampleMask = nullptr, 83 | .alphaToCoverageEnable = VK_FALSE, 84 | .alphaToOneEnable = VK_FALSE}; 85 | VkPipelineDepthStencilStateCreateInfo ds{ 86 | .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; 87 | VkPipelineColorBlendAttachmentState attachment{ 88 | .blendEnable = VK_FALSE, 89 | .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT 90 | | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT}; 91 | VkPipelineColorBlendStateCreateInfo cbs{ 92 | .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, 93 | .attachmentCount = 1, 94 | .pAttachments = &attachment}; 95 | VkDynamicState dynamic_states[] 96 | = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; 97 | VkPipelineDynamicStateCreateInfo dyn{ 98 | .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, 99 | .dynamicStateCount = 2, 100 | .pDynamicStates = dynamic_states}; 101 | 102 | VkFormat color_target = VK_FORMAT_R8G8B8A8_SRGB; 103 | VkPipelineRenderingCreateInfoKHR rendering_info{ 104 | .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR, 105 | .colorAttachmentCount = 1, 106 | .pColorAttachmentFormats = &color_target, 107 | }; 108 | 109 | VkGraphicsPipelineCreateInfo pipeline_info{ 110 | .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, 111 | .pNext = &rendering_info, 112 | .stageCount = 2, 113 | .pStages = stages, 114 | .pVertexInputState = &vi, 115 | .pInputAssemblyState = &ia, 116 | .pTessellationState = nullptr, 117 | .pViewportState = &v, 118 | .pRasterizationState = &rs, 119 | .pMultisampleState = &ms, 120 | .pDepthStencilState = &ds, 121 | .pColorBlendState = &cbs, 122 | .pDynamicState = &dyn, 123 | .layout = layout_, 124 | .renderPass = VK_NULL_HANDLE, 125 | .subpass = 0, 126 | .basePipelineHandle = VK_NULL_HANDLE, 127 | .basePipelineIndex = 0, 128 | }; 129 | vkCreateGraphicsPipelines( 130 | g_device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &pipeline_); 131 | 132 | vkDestroyShaderModule(g_device, ps_shader, nullptr); 133 | } 134 | 135 | void Fullscreen::reset() 136 | { 137 | if (pipeline_ != VK_NULL_HANDLE) 138 | { 139 | vkDestroyPipeline(g_device, pipeline_, nullptr); 140 | vkDestroyPipelineLayout(g_device, layout_, nullptr); 141 | } 142 | } 143 | 144 | void Fullscreen::render(VkCommandBuffer cb, Image const& color_target, void* push_constants) const 145 | { 146 | VkRect2D render_area = {.offset = {}, .extent = color_target.extent2_}; 147 | VkRenderingAttachmentInfoKHR attachment{ 148 | .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR, 149 | .imageView = color_target.image_view_, 150 | .imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 151 | .loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, 152 | .storeOp = VK_ATTACHMENT_STORE_OP_STORE, 153 | }; 154 | VkRenderingInfoKHR rendering_info{ 155 | .sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR, 156 | .renderArea = render_area, 157 | .layerCount = 1, 158 | .viewMask = 0, 159 | .colorAttachmentCount = 1, 160 | .pColorAttachments = &attachment, 161 | }; 162 | vkCmdBeginRenderingKHR(cb, &rendering_info); 163 | 164 | VkViewport viewport{.x = 0, 165 | .y = 0, 166 | .width = static_cast(render_area.extent.width), 167 | .height = static_cast(render_area.extent.height), 168 | .minDepth = 0.f, 169 | .maxDepth = 1.f}; 170 | vkCmdSetViewport(cb, 0, 1, &viewport); 171 | 172 | vkCmdSetScissor(cb, 0, 1, &render_area); 173 | 174 | vkCmdBindDescriptorSets(cb, 175 | VK_PIPELINE_BIND_POINT_GRAPHICS, 176 | layout_, 177 | 0, 178 | 1, 179 | &g_descriptor_set, 180 | 0, 181 | nullptr); 182 | 183 | vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_); 184 | 185 | vkCmdPushConstants(cb, 186 | layout_, 187 | VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_VERTEX_BIT, 188 | 0, 189 | pushconstant_size_, 190 | push_constants); 191 | 192 | vkCmdDraw(cb, 3, 1, 0, 0); 193 | 194 | vkCmdEndRenderingKHR(cb); 195 | } 196 | -------------------------------------------------------------------------------- /lib/Fullscreen.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "VkGlobals.hpp" 4 | 5 | class Image; 6 | 7 | class Fullscreen 8 | { 9 | public: 10 | void init(uint8_t const* shader_bytecode, 11 | size_t bytecode_size, 12 | uint8_t pushconstant_size); 13 | void reset(); 14 | 15 | void render(VkCommandBuffer cb, Image const& color_target, void* push_constants) const; 16 | 17 | private: 18 | VkPipeline pipeline_ = VK_NULL_HANDLE; 19 | VkPipelineLayout layout_ = VK_NULL_HANDLE; 20 | uint8_t pushconstant_size_ = 0; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/Image.cpp: -------------------------------------------------------------------------------- 1 | #include "Image.hpp" 2 | 3 | #include 4 | #include 5 | 6 | // Forward declare STBI calls to avoid including a massive header 7 | #include 8 | #include 9 | extern "C" 10 | { 11 | unsigned char* stbi_load(char const* filename, 12 | int* x, 13 | int* y, 14 | int* channels_in_file, 15 | int desired_channels); 16 | void stbi_image_free(void* retval_from_stbi_load); 17 | int stbi_write_png(char const* filename, 18 | int w, 19 | int h, 20 | int comp, 21 | const void* data, 22 | int stride_in_bytes); 23 | } 24 | 25 | using namespace flop; 26 | 27 | static int s_image_count; 28 | 29 | constexpr static VkImageSubresourceRange s_transfer_range{ 30 | .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, 31 | .baseMipLevel = 0, 32 | .levelCount = 1, 33 | .baseArrayLayer = 0, 34 | .layerCount = 1}; 35 | 36 | constexpr static VkImageSubresourceLayers s_subresource{ 37 | .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, 38 | .mipLevel = 0, 39 | .baseArrayLayer = 0, 40 | .layerCount = 1}; 41 | 42 | void Image::reset_count(uint32_t counter) 43 | { 44 | s_image_count = counter; 45 | } 46 | 47 | void init_from_data(void* image_data, int bytes_per_channel, Image& image) 48 | { 49 | VkBuffer staging_buffer; 50 | VmaAllocation staging_allocation; 51 | image.set_extents(); 52 | 53 | VmaAllocationCreateInfo staging_allocation_info{ 54 | .usage = VMA_MEMORY_USAGE_CPU_ONLY, 55 | }; 56 | VkBufferCreateInfo staging_info{ 57 | .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, 58 | .size = static_cast(image.width_ * image.height_ * 4 * bytes_per_channel), 59 | .usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT, 60 | .sharingMode = VK_SHARING_MODE_EXCLUSIVE, 61 | .queueFamilyIndexCount = 1, 62 | .pQueueFamilyIndices = &g_graphics_queue_index, 63 | }; 64 | vmaCreateBuffer(g_allocator, 65 | &staging_info, 66 | &staging_allocation_info, 67 | &staging_buffer, 68 | &staging_allocation, 69 | nullptr); 70 | void* data; 71 | vmaMapMemory(g_allocator, staging_allocation, &data); 72 | std::memcpy(data, image_data, image.width_ * image.height_ * 4 * bytes_per_channel); 73 | 74 | VkImageCreateInfo image_info{ 75 | .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, 76 | .imageType = VK_IMAGE_TYPE_2D, 77 | .format = bytes_per_channel == 1 ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R32G32B32A32_SFLOAT, 78 | .extent = image.extent3_, 79 | .mipLevels = 1, 80 | .arrayLayers = 1, 81 | .samples = VK_SAMPLE_COUNT_1_BIT, 82 | .tiling = VK_IMAGE_TILING_OPTIMAL, 83 | .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT 84 | | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, 85 | .sharingMode = VK_SHARING_MODE_EXCLUSIVE, 86 | .queueFamilyIndexCount = 1, 87 | .pQueueFamilyIndices = &g_graphics_queue_index, 88 | .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, 89 | }; 90 | VmaAllocationCreateInfo allocation_info{ 91 | .usage = VMA_MEMORY_USAGE_GPU_ONLY, 92 | }; 93 | vmaCreateImage(g_allocator, 94 | &image_info, 95 | &allocation_info, 96 | &image.image_, 97 | &image.allocation_, 98 | nullptr); 99 | 100 | VkCommandBuffer cb = g_command_buffers[0]; 101 | VkCommandBufferBeginInfo begin{ 102 | .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, 103 | .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, 104 | }; 105 | vkBeginCommandBuffer(cb, &begin); 106 | 107 | VkImageMemoryBarrier dst_transfer{ 108 | .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 109 | .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 110 | .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, 111 | .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, 112 | .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 113 | .srcQueueFamilyIndex = g_graphics_queue_index, 114 | .dstQueueFamilyIndex = g_graphics_queue_index, 115 | .image = image.image_, 116 | .subresourceRange = s_transfer_range}; 117 | vkCmdPipelineBarrier(cb, 118 | VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 119 | VK_PIPELINE_STAGE_TRANSFER_BIT, 120 | 0, 121 | 0, 122 | nullptr, 123 | 0, 124 | nullptr, 125 | 1, 126 | &dst_transfer); 127 | VkOffset3D offset{.x = 0, .y = 0, .z = 0}; 128 | VkBufferImageCopy copy{.bufferOffset = 0, 129 | .bufferRowLength = 0, 130 | .bufferImageHeight = 0, 131 | .imageSubresource = s_subresource, 132 | .imageOffset = offset, 133 | .imageExtent = image.extent3_}; 134 | vkCmdCopyBufferToImage(cb, 135 | staging_buffer, 136 | image.image_, 137 | VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 138 | 1, 139 | ©); 140 | 141 | VkImageMemoryBarrier src_transfer{ 142 | .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 143 | .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 144 | .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, 145 | .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 146 | .newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 147 | .srcQueueFamilyIndex = g_graphics_queue_index, 148 | .dstQueueFamilyIndex = g_graphics_queue_index, 149 | .image = image.image_, 150 | .subresourceRange = s_transfer_range}; 151 | vkCmdPipelineBarrier(cb, 152 | VK_PIPELINE_STAGE_TRANSFER_BIT, 153 | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 154 | 0, 155 | 0, 156 | nullptr, 157 | 0, 158 | nullptr, 159 | 1, 160 | &src_transfer); 161 | vkEndCommandBuffer(cb); 162 | VkSubmitInfo submit{ 163 | .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, 164 | .waitSemaphoreCount = 0, 165 | .commandBufferCount = 1, 166 | .pCommandBuffers = &cb, 167 | .signalSemaphoreCount = 0, 168 | }; 169 | vkQueueSubmit(g_graphics_queue, 1, &submit, VK_NULL_HANDLE); 170 | vkDeviceWaitIdle(g_device); 171 | 172 | vmaUnmapMemory(g_allocator, staging_allocation); 173 | vmaDestroyBuffer(g_allocator, staging_buffer, staging_allocation); 174 | 175 | VkImageSubresourceRange range{ 176 | .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, 177 | .baseMipLevel = 0, 178 | .levelCount = 1, 179 | .baseArrayLayer = 0, 180 | .layerCount = 1, 181 | }; 182 | VkImageViewCreateInfo view_info{ 183 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, 184 | .image = image.image_, 185 | .viewType = VK_IMAGE_VIEW_TYPE_2D, 186 | .format = image_info.format, 187 | .subresourceRange = range}; 188 | vkCreateImageView(g_device, &view_info, nullptr, &image.image_view_); 189 | 190 | image.index_ = s_image_count++; 191 | 192 | VkDescriptorImageInfo descriptor_info{ 193 | .imageView = image.image_view_, 194 | .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; 195 | VkWriteDescriptorSet descriptor_write{ 196 | .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, 197 | .dstSet = g_descriptor_set, 198 | .dstBinding = 0, 199 | .dstArrayElement = image.index_, 200 | .descriptorCount = 1, 201 | .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 202 | .pImageInfo = &descriptor_info, 203 | }; 204 | vkUpdateDescriptorSets(g_device, 1, &descriptor_write, 0, nullptr); 205 | } 206 | 207 | Image Image::create_from_exr(char const* path) 208 | { 209 | Image image; 210 | image.hdr_ = true; 211 | 212 | float* rgba = nullptr; 213 | char const* error = nullptr; 214 | if (LoadEXRWithLayer(&rgba, &image.width_, &image.height_, path, nullptr, &error) < 0 || !rgba) 215 | { 216 | if (error) 217 | { 218 | std::cout << "Error loading EXR " << path << ":\n" << error << '\n'; 219 | } 220 | else 221 | { 222 | std::cout << "Error loading EXR " << path << '\n'; 223 | } 224 | std::exit(1); 225 | } 226 | image.channels_ = 4; 227 | 228 | init_from_data(rgba, 4, image); 229 | std::free(rgba); 230 | 231 | return image; 232 | } 233 | 234 | Image Image::create_from_non_exr(char const* path) 235 | { 236 | Image image; 237 | 238 | unsigned char* stb_data 239 | = stbi_load(path, &image.width_, &image.height_, &image.channels_, 4); 240 | 241 | if (!stb_data) 242 | { 243 | std::cout << "Error loading PNG " << path << ":\n"; 244 | std::exit(1); 245 | } 246 | 247 | init_from_data(stb_data, 1, image); 248 | stbi_image_free(stb_data); 249 | 250 | return image; 251 | } 252 | 253 | Image Image::create(const Image& other, VkFormat format, bool attachment) 254 | { 255 | // Create an RGB image with matching dimensions to the supplied image 256 | Image image; 257 | image.width_ = other.width_; 258 | image.height_ = other.height_; 259 | image.writable_ = true; 260 | image.set_extents(); 261 | 262 | VkImageCreateInfo image_info{ 263 | .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, 264 | .imageType = VK_IMAGE_TYPE_2D, 265 | .format = format, 266 | .extent = image.extent3_, 267 | .mipLevels = 1, 268 | .arrayLayers = 1, 269 | .samples = VK_SAMPLE_COUNT_1_BIT, 270 | .tiling = VK_IMAGE_TILING_OPTIMAL, 271 | .usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT 272 | | VK_IMAGE_USAGE_TRANSFER_DST_BIT 273 | | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, 274 | .sharingMode = VK_SHARING_MODE_EXCLUSIVE, 275 | .queueFamilyIndexCount = 1, 276 | .pQueueFamilyIndices = &g_graphics_queue_index, 277 | .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, 278 | }; 279 | if (attachment) 280 | { 281 | if (format == VK_FORMAT_R8G8B8A8_SRGB) 282 | { 283 | image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT 284 | | VK_IMAGE_USAGE_SAMPLED_BIT 285 | | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; 286 | } 287 | else 288 | { 289 | image_info.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; 290 | } 291 | } 292 | 293 | VmaAllocationCreateInfo allocation_info{ 294 | .usage = VMA_MEMORY_USAGE_GPU_ONLY, 295 | }; 296 | vmaCreateImage(g_allocator, 297 | &image_info, 298 | &allocation_info, 299 | &image.image_, 300 | &image.allocation_, 301 | nullptr); 302 | 303 | VkImageSubresourceRange range{ 304 | .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, 305 | .baseMipLevel = 0, 306 | .levelCount = 1, 307 | .baseArrayLayer = 0, 308 | .layerCount = 1, 309 | }; 310 | VkImageViewCreateInfo view_info{ 311 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, 312 | .image = image.image_, 313 | .viewType = VK_IMAGE_VIEW_TYPE_2D, 314 | .format = image_info.format, 315 | .subresourceRange = range}; 316 | vkCreateImageView(g_device, &view_info, nullptr, &image.image_view_); 317 | 318 | image.index_ = s_image_count++; 319 | 320 | VkDescriptorImageInfo descriptor_info{ 321 | .imageView = image.image_view_, .imageLayout = VK_IMAGE_LAYOUT_GENERAL}; 322 | VkWriteDescriptorSet descriptor_write{ 323 | .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, 324 | .dstSet = g_descriptor_set, 325 | .dstBinding = 1, 326 | .dstArrayElement = image.index_, 327 | .descriptorCount = 1, 328 | .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 329 | .pImageInfo = &descriptor_info, 330 | }; 331 | if (image_info.usage & VK_IMAGE_USAGE_STORAGE_BIT) 332 | { 333 | vkUpdateDescriptorSets(g_device, 1, &descriptor_write, 0, nullptr); 334 | } 335 | descriptor_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; 336 | descriptor_write.dstBinding = 0; 337 | descriptor_write.descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; 338 | vkUpdateDescriptorSets(g_device, 1, &descriptor_write, 0, nullptr); 339 | 340 | return image; 341 | } 342 | 343 | void Image::set_extents() 344 | { 345 | extent2_.width = width_; 346 | extent2_.height = height_; 347 | extent3_.width = width_; 348 | extent3_.height = height_; 349 | extent3_.depth = 1; 350 | } 351 | 352 | Image Image::create_readback(Image const& other, VkFormat format) 353 | { 354 | Image image; 355 | image.width_ = other.width_; 356 | image.height_ = other.height_; 357 | image.set_extents(); 358 | image.writable_ = true; 359 | 360 | VkImageCreateInfo image_info{ 361 | .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, 362 | .imageType = VK_IMAGE_TYPE_2D, 363 | .format = format, 364 | .extent = image.extent3_, 365 | .mipLevels = 1, 366 | .arrayLayers = 1, 367 | .samples = VK_SAMPLE_COUNT_1_BIT, 368 | .tiling = VK_IMAGE_TILING_LINEAR, 369 | .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT, 370 | .sharingMode = VK_SHARING_MODE_EXCLUSIVE, 371 | .queueFamilyIndexCount = 1, 372 | .pQueueFamilyIndices = &g_graphics_queue_index, 373 | .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, 374 | }; 375 | 376 | VmaAllocationCreateInfo allocation_info{ 377 | .usage = VMA_MEMORY_USAGE_GPU_TO_CPU, 378 | }; 379 | vmaCreateImage(g_allocator, 380 | &image_info, 381 | &allocation_info, 382 | &image.image_, 383 | &image.allocation_, 384 | nullptr); 385 | return image; 386 | } 387 | 388 | void Image::reset() 389 | { 390 | if (allocation_ != VK_NULL_HANDLE) 391 | { 392 | vmaDestroyImage(g_allocator, image_, allocation_); 393 | if (image_view_ != VK_NULL_HANDLE) 394 | { 395 | vkDestroyImageView(g_device, image_view_, nullptr); 396 | image_view_ = VK_NULL_HANDLE; 397 | } 398 | allocation_ = VK_NULL_HANDLE; 399 | image_ = VK_NULL_HANDLE; 400 | width_ = 0; 401 | height_ = 0; 402 | channels_ = 0; 403 | hdr_ = false; 404 | } 405 | } 406 | 407 | void Image::readback(VkCommandBuffer cb, Image& readback) 408 | { 409 | VkImageCopy copy{.srcSubresource = s_subresource, 410 | .srcOffset = {.x = 0, .y = 0, .z = 0}, 411 | .dstSubresource = s_subresource, 412 | .dstOffset = {.x = 0, .y = 0, .z = 0}, 413 | .extent = readback.extent3_}; 414 | vkCmdCopyImage(cb, 415 | image_, 416 | VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 417 | readback.image_, 418 | VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 419 | 1, 420 | ©); 421 | } 422 | 423 | void Image::write(std::string const& path) 424 | { 425 | // Query row stride 426 | VkImageSubresource subresource{ 427 | .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = 0, .arrayLayer = 0}; 428 | VkSubresourceLayout layout; 429 | vkGetImageSubresourceLayout(g_device, image_, &subresource, &layout); 430 | 431 | uint8_t* data; 432 | vmaMapMemory(g_allocator, allocation_, reinterpret_cast(&data)); 433 | data += layout.offset; 434 | stbi_write_png(path.c_str(), width_, height_, 4, data, layout.rowPitch); 435 | vmaUnmapMemory(g_allocator, allocation_); 436 | } 437 | 438 | VkImageMemoryBarrier Image::start_barrier(VkImageLayout layout) 439 | { 440 | layout_ = layout; 441 | return {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 442 | .srcAccessMask = VK_ACCESS_NONE_KHR, 443 | .dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 444 | .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, 445 | .newLayout = layout_, 446 | .srcQueueFamilyIndex = g_graphics_queue_index, 447 | .dstQueueFamilyIndex = g_graphics_queue_index, 448 | .image = image_, 449 | .subresourceRange = s_transfer_range}; 450 | } 451 | 452 | VkImageMemoryBarrier Image::war_barrier() 453 | { 454 | return {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 455 | .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT, 456 | .dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 457 | .oldLayout = VK_IMAGE_LAYOUT_GENERAL, 458 | .newLayout = VK_IMAGE_LAYOUT_GENERAL, 459 | .srcQueueFamilyIndex = g_graphics_queue_index, 460 | .dstQueueFamilyIndex = g_graphics_queue_index, 461 | .image = image_, 462 | .subresourceRange = s_transfer_range}; 463 | } 464 | 465 | VkImageMemoryBarrier Image::raw_barrier(VkAccessFlags access) 466 | { 467 | VkImageLayout old = layout_; 468 | layout_ = VK_IMAGE_LAYOUT_GENERAL; 469 | return {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 470 | .srcAccessMask = access, 471 | .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, 472 | .oldLayout = old, 473 | .newLayout = VK_IMAGE_LAYOUT_GENERAL, 474 | .srcQueueFamilyIndex = g_graphics_queue_index, 475 | .dstQueueFamilyIndex = g_graphics_queue_index, 476 | .image = image_, 477 | .subresourceRange = s_transfer_range}; 478 | } 479 | 480 | VkImageMemoryBarrier Image::rar_barrier(VkImageLayout layout) 481 | { 482 | VkImageLayout old = layout_; 483 | layout_ = layout; 484 | return {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 485 | .srcAccessMask = VK_ACCESS_MEMORY_READ_BIT, 486 | .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, 487 | .oldLayout = old, 488 | .newLayout = layout_, 489 | .srcQueueFamilyIndex = g_graphics_queue_index, 490 | .dstQueueFamilyIndex = g_graphics_queue_index, 491 | .image = image_, 492 | .subresourceRange = s_transfer_range}; 493 | } 494 | 495 | VkImageMemoryBarrier Image::waw_barrier() 496 | { 497 | VkImageLayout old = layout_; 498 | layout_ = VK_IMAGE_LAYOUT_GENERAL; 499 | return {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 500 | .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 501 | .dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 502 | .oldLayout = old, 503 | .newLayout = layout_, 504 | .srcQueueFamilyIndex = g_graphics_queue_index, 505 | .dstQueueFamilyIndex = g_graphics_queue_index, 506 | .image = image_, 507 | .subresourceRange = s_transfer_range}; 508 | } 509 | 510 | VkImageMemoryBarrier Image::blit_barrier() 511 | { 512 | VkImageLayout old = layout_; 513 | layout_ = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; 514 | return {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 515 | .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 516 | .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, 517 | .oldLayout = old, 518 | .newLayout = layout_, 519 | .srcQueueFamilyIndex = g_graphics_queue_index, 520 | .dstQueueFamilyIndex = g_graphics_queue_index, 521 | .image = image_, 522 | .subresourceRange = s_transfer_range}; 523 | } 524 | 525 | VkImageMemoryBarrier Image::sample_barrier(VkAccessFlags src_access) 526 | { 527 | VkImageLayout old = layout_; 528 | layout_ = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; 529 | return {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 530 | .srcAccessMask = src_access, 531 | .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, 532 | .oldLayout = old, 533 | .newLayout = layout_, 534 | .srcQueueFamilyIndex = g_graphics_queue_index, 535 | .dstQueueFamilyIndex = g_graphics_queue_index, 536 | .image = image_, 537 | .subresourceRange = s_transfer_range}; 538 | } 539 | 540 | VkImageMemoryBarrier Image::readback_barrier() 541 | { 542 | VkImageLayout old = layout_; 543 | layout_ = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; 544 | return {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 545 | .srcAccessMask = VK_ACCESS_NONE_KHR, 546 | .dstAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 547 | .oldLayout = old, 548 | .newLayout = layout_, 549 | .srcQueueFamilyIndex = g_graphics_queue_index, 550 | .dstQueueFamilyIndex = g_graphics_queue_index, 551 | .image = image_, 552 | .subresourceRange = s_transfer_range}; 553 | } 554 | -------------------------------------------------------------------------------- /lib/Image.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "VkGlobals.hpp" 4 | 5 | #include 6 | 7 | class Image 8 | { 9 | public: 10 | static void reset_count(uint32_t counter = 0); 11 | 12 | // Decodes an image and uploads it to the GPU. The result is provided in the 13 | // shader read-only layout. 14 | static Image create_from_non_exr(char const* path); 15 | static Image create_from_exr(char const* path); 16 | 17 | // Creates a device image with matching dimensions. The image layout that 18 | // results is undefined. 19 | static Image 20 | create(const Image& other, VkFormat format = VK_FORMAT_R32G32B32A32_SFLOAT, bool attachment = false); 21 | 22 | // Creates a host image with matching dimensions suitable for readback. 23 | static Image create_readback(Image const& other, 24 | VkFormat format = VK_FORMAT_R8G8B8A8_SRGB); 25 | 26 | void reset(); 27 | float aspect() const 28 | { 29 | return static_cast(width_) / height_; 30 | } 31 | 32 | void readback(VkCommandBuffer cb, Image& readback); 33 | void write(std::string const& path); 34 | 35 | void set_extents(); 36 | 37 | VkImageMemoryBarrier start_barrier(VkImageLayout layout = VK_IMAGE_LAYOUT_GENERAL); 38 | VkImageMemoryBarrier blit_barrier(); 39 | VkImageMemoryBarrier war_barrier(); 40 | VkImageMemoryBarrier raw_barrier(VkAccessFlags access = VK_ACCESS_MEMORY_WRITE_BIT); 41 | VkImageMemoryBarrier rar_barrier(VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); 42 | VkImageMemoryBarrier waw_barrier(); 43 | VkImageMemoryBarrier sample_barrier(VkAccessFlags src_access = VK_ACCESS_MEMORY_WRITE_BIT); 44 | VkImageMemoryBarrier readback_barrier(); 45 | 46 | VkImage image_ = VK_NULL_HANDLE; 47 | VkImageView image_view_ = VK_NULL_HANDLE; 48 | VmaAllocation allocation_ = VK_NULL_HANDLE; 49 | VkImageLayout layout_ = VK_IMAGE_LAYOUT_UNDEFINED; 50 | 51 | VkExtent2D extent2_ = {}; 52 | VkExtent3D extent3_ = {}; 53 | int32_t width_ = 0; 54 | int32_t height_ = 0; 55 | int32_t channels_ = 0; 56 | uint32_t index_ = 0; 57 | bool hdr_ = false; 58 | bool writable_ = false; 59 | }; 60 | -------------------------------------------------------------------------------- /lib/Kernel.cpp: -------------------------------------------------------------------------------- 1 | #include "Kernel.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | VkPipelineLayout s_kernel_layout; 8 | VkPipelineLayout s_compare_kernel_layout; 9 | 10 | using namespace flop; 11 | 12 | VkShaderModule Kernel::compile_shader(uint8_t const* data, size_t size) 13 | { 14 | VkShaderModule shader_module; 15 | 16 | VkShaderModuleCreateInfo info{ 17 | .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, 18 | .codeSize = size, 19 | .pCode = reinterpret_cast(data)}; 20 | if (vkCreateShaderModule(g_device, &info, nullptr, &shader_module) 21 | != VK_SUCCESS) 22 | { 23 | std::cout << "Failed to create shader module.\n"; 24 | } 25 | return shader_module; 26 | } 27 | 28 | Kernel Kernel::create(uint8_t const* data, 29 | size_t size, 30 | int thread_count_x, 31 | int thread_count_y, 32 | bool is_compare_kernel) 33 | { 34 | Kernel out; 35 | out.thread_count_x_ = thread_count_x; 36 | out.thread_count_y_ = thread_count_y; 37 | 38 | VkShaderModule shader_module = compile_shader(data, size); 39 | 40 | if (s_kernel_layout == VK_NULL_HANDLE) 41 | { 42 | VkPushConstantRange push_constant_range{ 43 | .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, 44 | .offset = 0, 45 | .size = sizeof(PushConstants)}; 46 | 47 | VkPipelineLayoutCreateInfo pipeline_layout_info{ 48 | .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, 49 | .setLayoutCount = 1, 50 | .pSetLayouts = &g_descriptor_set_layout, 51 | .pushConstantRangeCount = 1, 52 | .pPushConstantRanges = &push_constant_range}; 53 | 54 | vkCreatePipelineLayout( 55 | g_device, &pipeline_layout_info, nullptr, &s_kernel_layout); 56 | 57 | push_constant_range.size = sizeof(ComparePushConstants); 58 | vkCreatePipelineLayout( 59 | g_device, &pipeline_layout_info, nullptr, &s_compare_kernel_layout); 60 | } 61 | 62 | VkComputePipelineCreateInfo pipeline_info{ 63 | .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, 64 | .stage = { 65 | .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, 66 | .stage = VK_SHADER_STAGE_COMPUTE_BIT, 67 | .module = shader_module, 68 | .pName = "CSMain", 69 | .pSpecializationInfo = nullptr, 70 | }, 71 | .layout = is_compare_kernel ? s_compare_kernel_layout : s_kernel_layout, 72 | }; 73 | vkCreateComputePipelines( 74 | g_device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &out.pipeline_); 75 | 76 | return out; 77 | } 78 | 79 | uint32_t div_round_up(int a, int b) 80 | { 81 | return (a + b - 1) / b; 82 | } 83 | 84 | void Kernel::dispatch(VkCommandBuffer cb, Image const& input, Image const& output) 85 | { 86 | vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_); 87 | vkCmdBindDescriptorSets(cb, 88 | VK_PIPELINE_BIND_POINT_COMPUTE, 89 | s_kernel_layout, 90 | 0, 91 | 1, 92 | &g_descriptor_set, 93 | 0, 94 | nullptr); 95 | 96 | PushConstants push_constants{.extent = {input.width_, input.height_}, 97 | .input = input.index_, 98 | .output = output.index_}; 99 | vkCmdPushConstants(cb, 100 | s_kernel_layout, 101 | VK_SHADER_STAGE_COMPUTE_BIT, 102 | 0, 103 | sizeof(PushConstants), 104 | &push_constants); 105 | vkCmdDispatch(cb, 106 | div_round_up(input.width_, thread_count_x_), 107 | div_round_up(input.height_, thread_count_y_), 108 | 1); 109 | } 110 | 111 | void Kernel::dispatch(VkCommandBuffer cb, 112 | Image const& input1, 113 | Image const& input2, 114 | Image const& output) 115 | { 116 | vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_); 117 | vkCmdBindDescriptorSets(cb, 118 | VK_PIPELINE_BIND_POINT_COMPUTE, 119 | s_compare_kernel_layout, 120 | 0, 121 | 1, 122 | &g_descriptor_set, 123 | 0, 124 | nullptr); 125 | 126 | ComparePushConstants push_constants{.extent = {input1.width_, input1.height_}, 127 | .input1 = input1.index_, 128 | .input2 = input2.index_, 129 | .output1 = output.index_}; 130 | vkCmdPushConstants(cb, 131 | s_compare_kernel_layout, 132 | VK_SHADER_STAGE_COMPUTE_BIT, 133 | 0, 134 | sizeof(ComparePushConstants), 135 | &push_constants); 136 | vkCmdDispatch(cb, 137 | div_round_up(input1.width_, thread_count_x_), 138 | div_round_up(input1.height_, thread_count_y_), 139 | 1); 140 | } 141 | 142 | void Kernel::dispatch(VkCommandBuffer cb, 143 | Image const& input, 144 | Image const& output, 145 | Buffer const& buffer) 146 | { 147 | vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_); 148 | vkCmdBindDescriptorSets(cb, 149 | VK_PIPELINE_BIND_POINT_COMPUTE, 150 | s_compare_kernel_layout, 151 | 0, 152 | 1, 153 | &g_descriptor_set, 154 | 0, 155 | nullptr); 156 | 157 | ComparePushConstants push_constants{.extent = {input.width_, input.height_}, 158 | .input1 = input.index_, 159 | .input2 = output.index_, 160 | .output1 = buffer.index_}; 161 | vkCmdPushConstants(cb, 162 | s_compare_kernel_layout, 163 | VK_SHADER_STAGE_COMPUTE_BIT, 164 | 0, 165 | sizeof(ComparePushConstants), 166 | &push_constants); 167 | vkCmdDispatch(cb, 168 | div_round_up(input.width_, thread_count_x_), 169 | div_round_up(input.height_, thread_count_y_), 170 | 1); 171 | } 172 | 173 | void Kernel::dispatch(VkCommandBuffer cb, 174 | Image const& input1, 175 | Image const& input2, 176 | Image const& output1, 177 | Image const& output2) 178 | { 179 | vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_); 180 | vkCmdBindDescriptorSets(cb, 181 | VK_PIPELINE_BIND_POINT_COMPUTE, 182 | s_compare_kernel_layout, 183 | 0, 184 | 1, 185 | &g_descriptor_set, 186 | 0, 187 | nullptr); 188 | 189 | ComparePushConstants push_constants{.extent = {input1.width_, input1.height_}, 190 | .input1 = input1.index_, 191 | .input2 = input2.index_, 192 | .output1 = output1.index_, 193 | .output2 = output2.index_}; 194 | vkCmdPushConstants(cb, 195 | s_compare_kernel_layout, 196 | VK_SHADER_STAGE_COMPUTE_BIT, 197 | 0, 198 | sizeof(ComparePushConstants), 199 | &push_constants); 200 | vkCmdDispatch(cb, 201 | div_round_up(input1.width_, thread_count_x_), 202 | div_round_up(input1.height_, thread_count_y_), 203 | 1); 204 | } 205 | 206 | void Kernel::dispatch(VkCommandBuffer cb, Image const& input, Buffer const& output) 207 | { 208 | vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_); 209 | vkCmdBindDescriptorSets(cb, 210 | VK_PIPELINE_BIND_POINT_COMPUTE, 211 | s_kernel_layout, 212 | 0, 213 | 1, 214 | &g_descriptor_set, 215 | 0, 216 | nullptr); 217 | 218 | PushConstants push_constants{.extent = {input.width_, input.height_}, 219 | .input = input.index_, 220 | .output = output.index_}; 221 | vkCmdPushConstants(cb, 222 | s_compare_kernel_layout, 223 | VK_SHADER_STAGE_COMPUTE_BIT, 224 | 0, 225 | sizeof(PushConstants), 226 | &push_constants); 227 | vkCmdDispatch(cb, 228 | div_round_up(input.width_, thread_count_x_), 229 | div_round_up(input.height_, thread_count_y_), 230 | 1); 231 | } 232 | -------------------------------------------------------------------------------- /lib/Kernel.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "VkGlobals.hpp" 4 | 5 | #include "Buffer.hpp" 6 | #include "Image.hpp" 7 | 8 | class Kernel 9 | { 10 | public: 11 | struct PushConstants 12 | { 13 | int32_t extent[2]; 14 | uint32_t input; 15 | uint32_t output; 16 | }; 17 | 18 | struct ComparePushConstants 19 | { 20 | int32_t extent[2]; 21 | uint32_t input1; 22 | uint32_t input2; 23 | uint32_t output1; 24 | // output2 isn't always used 25 | uint32_t output2; 26 | }; 27 | 28 | static void init_dxc(); 29 | static Kernel create(uint8_t const* data, 30 | size_t size, 31 | int thread_count_x, 32 | int thread_count_y, 33 | bool is_compare_kernel); 34 | static VkShaderModule compile_shader(uint8_t const* data, size_t size); 35 | 36 | void dispatch(VkCommandBuffer cb, Image const& input, Image const& output); 37 | void dispatch(VkCommandBuffer cb, 38 | Image const& input1, 39 | Image const& input2, 40 | Image const& output); 41 | void dispatch(VkCommandBuffer cb, 42 | Image const& input, 43 | Image const& output, 44 | Buffer const& buffer); 45 | void dispatch(VkCommandBuffer cb, 46 | Image const& input1, 47 | Image const& input2, 48 | Image const& output1, 49 | Image const& output2); 50 | void dispatch(VkCommandBuffer cb, Image const& input, Buffer const& output); 51 | 52 | private: 53 | VkPipeline pipeline_ = VK_NULL_HANDLE; 54 | int thread_count_x_ = 0; 55 | int thread_count_y_ = 0; 56 | }; 57 | -------------------------------------------------------------------------------- /lib/STB.cpp: -------------------------------------------------------------------------------- 1 | #define STB_IMAGE_IMPLEMENTATION 2 | #include 3 | 4 | #define STB_IMAGE_WRITE_IMPLEMENTATION 5 | #include 6 | -------------------------------------------------------------------------------- /lib/VMA.cpp: -------------------------------------------------------------------------------- 1 | #define VMA_IMPLEMENTATION 2 | #define VMA_STATIC_VULKAN_FUNCTIONS 0 3 | #define VMA_DYNAMIC_VULKAN_FUNCTIONS 1 4 | #include 5 | #include 6 | -------------------------------------------------------------------------------- /lib/VkGlobals.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace flop 9 | { 10 | inline VkInstance g_instance = VK_NULL_HANDLE; 11 | inline VkPhysicalDevice g_physical_device = VK_NULL_HANDLE; 12 | inline VkPhysicalDeviceProperties g_physical_device_props = {}; 13 | inline PFN_vkCmdBeginDebugUtilsLabelEXT vkCmdBeginDebugUtilsLabel = nullptr; 14 | inline PFN_vkCmdEndDebugUtilsLabelEXT vkCmdEndDebugUtilsLabel = nullptr; 15 | inline uint32_t g_graphics_queue_index = -0u; 16 | inline uint32_t g_compute_queue_index = -0u; 17 | inline VkQueue g_graphics_queue = VK_NULL_HANDLE; 18 | inline VkQueue g_compute_queue = VK_NULL_HANDLE; 19 | inline VkDevice g_device = VK_NULL_HANDLE; 20 | inline VmaAllocator g_allocator = VK_NULL_HANDLE; 21 | inline VkCommandPool g_command_pool = VK_NULL_HANDLE; 22 | inline VkCommandBuffer g_command_buffers[] 23 | = {VK_NULL_HANDLE, VK_NULL_HANDLE, VK_NULL_HANDLE, VK_NULL_HANDLE}; 24 | inline VkDescriptorPool g_descriptor_pool = VK_NULL_HANDLE; 25 | inline VkDescriptorSetLayout g_descriptor_set_layout = VK_NULL_HANDLE; 26 | inline VkDescriptorSet g_descriptor_set = VK_NULL_HANDLE; 27 | 28 | // Helper function to retrieve a count, and then populate a vector with 29 | // count entries 30 | template 31 | std::vector vk_enumerate(F fn, Ts... args) 32 | { 33 | uint32_t count; 34 | fn(args..., &count, nullptr); 35 | 36 | std::vector out{count}; 37 | fn(args..., &count, out.data()); 38 | 39 | return out; 40 | } 41 | } // namespace flop 42 | -------------------------------------------------------------------------------- /lib/imgui.ini: -------------------------------------------------------------------------------- 1 | [Window][Debug##Default] 2 | Pos=60,60 3 | Size=400,400 4 | Collapsed=0 5 | 6 | [Window][Flop] 7 | Pos=960,0 8 | Size=960,540 9 | Collapsed=0 10 | 11 | -------------------------------------------------------------------------------- /shaders/BinToHex.cmake: -------------------------------------------------------------------------------- 1 | # FileEmbed.cmake 2 | # LICENSE MIT 3 | # Reproduced from https://gitlab.com/jhamberg/cmake-examples/-/blob/master/cmake/FileEmbed.cmake 4 | # with modifications to support C++ 5 | 6 | get_filename_component(base_filename ${INPUT_PATH} NAME) 7 | string(MAKE_C_IDENTIFIER ${base_filename} c_name) 8 | file(READ ${INPUT_PATH} content HEX) 9 | 10 | # Separate into individual bytes. 11 | string(REGEX MATCHALL "([A-Fa-f0-9][A-Fa-f0-9])" SEPARATED_HEX ${content}) 12 | 13 | set(output_c "") 14 | 15 | set(counter 0) 16 | foreach (hex IN LISTS SEPARATED_HEX) 17 | string(APPEND output_c "0x${hex},") 18 | MATH(EXPR counter "${counter}+1") 19 | if (counter GREATER 16) 20 | string(APPEND output_c "\n ") 21 | set(counter 0) 22 | endif () 23 | endforeach () 24 | 25 | set(output_c " 26 | #include \"${c_name}.h\" 27 | uint8_t ${c_name}_data[] = { 28 | ${output_c} 29 | }\; 30 | unsigned ${c_name}_size = sizeof(${c_name}_data)\; 31 | ") 32 | 33 | set(output_h " 34 | #ifndef ${c_name}_H 35 | #define ${c_name}_H 36 | #include \"stdint.h\" 37 | #ifdef __cplusplus 38 | extern \"C\" { 39 | #endif 40 | extern uint8_t ${c_name}_data[]\; 41 | extern unsigned ${c_name}_size\; 42 | #ifdef __cplusplus 43 | } 44 | #endif 45 | #endif // ${c_name}_H 46 | ") 47 | 48 | file(WRITE ${OUTPUT_PATH}/${c_name}.c ${output_c}) 49 | file(WRITE ${OUTPUT_PATH}/${c_name}.h ${output_h}) 50 | -------------------------------------------------------------------------------- /shaders/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | 3 | FetchContent_Declare( 4 | dxcompiler 5 | # Release info: https://github.com/microsoft/DirectXShaderCompiler/releases/tag/v1.6.2112 6 | URL https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.6.2112/dxc_2021_12_08.zip 7 | ) 8 | if(NOT dxcompiler_POPULATED) 9 | FetchContent_Populate(dxcompiler) 10 | endif() 11 | 12 | set(SHADER_BIN ${CMAKE_BINARY_DIR}/shader_lib) 13 | if(NOT EXISTS ${SHADER_BIN}) 14 | file(MAKE_DIRECTORY ${SHADER_BIN}) 15 | endif() 16 | 17 | set(FLOP_SPIRV) 18 | set(FLOP_SPIRV_HEX "") 19 | set(CMAKE_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/BinToHex.cmake) 20 | function(add_spv SOURCE TARGET PROFILE ENTRY) 21 | set(OUTFILE ${CMAKE_CURRENT_BINARY_DIR}/${TARGET}) 22 | set(SOURCEFILE ${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE}) 23 | set(CMD_ARGS "-spirv -T ${PROFILE} -E ${ENTRY} -Fo \"${OUTFILE}\" ${ARGV4} \"${SOURCEFILE}\"") 24 | get_filename_component(BASE ${OUTFILE} NAME) 25 | string(MAKE_C_IDENTIFIER ${BASE} HEX_SOURCE) 26 | string(APPEND FLOP_SPIRV_HEX " ${HEX_SOURCE}.c") 27 | set(FLOP_SPIRV_HEX ${FLOP_SPIRV_HEX} PARENT_SCOPE) 28 | 29 | add_custom_command( 30 | OUTPUT ${OUTFILE} 31 | COMMAND ${dxcompiler_SOURCE_DIR}/bin/x64/dxc.exe -spirv -T ${PROFILE} -E ${ENTRY} -Fo ${OUTFILE} ${ARGV4} ${SOURCEFILE} 32 | COMMAND ${CMAKE_COMMAND} -DINPUT_PATH=${OUTFILE} -DOUTPUT_PATH=${SHADER_BIN} -P ${CMAKE_SCRIPT} 33 | MAIN_DEPENDENCY ${SOURCE} 34 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 35 | COMMENT "Compiling shader ${SOURCE}" 36 | VERBATIM 37 | USES_TERMINAL ON 38 | ) 39 | list(APPEND FLOP_SPIRV ${OUTFILE}) 40 | set(FLOP_SPIRV ${FLOP_SPIRV} PARENT_SCOPE) 41 | endfunction() 42 | 43 | add_spv(CSFFilter.hlsl CSFFilterX.spv cs_6_6 CSMain "-DDIRECTION_X") 44 | add_spv(CSFFilter.hlsl CSFFilterY.spv cs_6_6 CSMain "-DDIRECTION_Y") 45 | add_spv(ColorCompare.hlsl ColorCompare.spv cs_6_6 CSMain) 46 | add_spv(ErrorColorMap.hlsl ErrorColorMap.spv ps_6_6 PSMain) 47 | add_spv(FeatureFilter.hlsl FeatureFilterX.spv cs_6_6 CSMain "-DDIRECTION_X") 48 | add_spv(FeatureFilter.hlsl FeatureFilterY.spv cs_6_6 CSMain "-DDIRECTION_Y") 49 | add_spv(FullscreenVS.hlsl FullscreenVS.spv vs_6_6 VSMain) 50 | add_spv(Preview.hlsl PreviewVS.spv vs_6_6 VSMain) 51 | add_spv(Preview.hlsl PreviewPS.spv ps_6_6 PSMain) 52 | add_spv(Preview.hlsl PreviewPSColorMap.spv ps_6_6 PSMain "-DCOLORMAP") 53 | add_spv(Summarize.hlsl Summarize.spv cs_6_6 CSMain) 54 | add_spv(Tonemap.hlsl Tonemap.spv ps_6_6 PSMain) 55 | add_spv(YyCxCz.hlsl YyCxCz.spv ps_6_6 PSMain) 56 | 57 | configure_file(HexToLib.cmake ${SHADER_BIN}/CMakeLists.txt) 58 | 59 | add_custom_command( 60 | OUTPUT ${SHADER_BIN}/$/flop_shaders_hex${CMAKE_STATIC_LIBRARY_SUFFIX} 61 | COMMAND ${CMAKE_COMMAND} -E rm -rf ${SHADER_BIN}/CMakeCache.txt 62 | COMMAND ${CMAKE_COMMAND} -E rm -rf ${SHADER_BIN}/CMakeFiles 63 | COMMAND ${CMAKE_COMMAND} -E rm -rf ${SHADER_BIN}/Debug 64 | COMMAND ${CMAKE_COMMAND} -E rm -rf ${SHADER_BIN}/Release 65 | COMMAND ${CMAKE_COMMAND} -E rm -rf ${SHADER_BIN}/flop_shaders_hex.dir 66 | COMMAND ${CMAKE_COMMAND} -S ${SHADER_BIN} -B ${SHADER_BIN} -G ${CMAKE_GENERATOR} 67 | COMMAND ${CMAKE_COMMAND} --build ${SHADER_BIN} --target flop_shaders_hex --config $ 68 | DEPENDS ${FLOP_SPIRV} 69 | ) 70 | add_custom_target( 71 | flop_shaders_hex_lib 72 | DEPENDS ${SHADER_BIN}/$/flop_shaders_hex${CMAKE_STATIC_LIBRARY_SUFFIX}) 73 | add_library(flop_shaders INTERFACE) 74 | target_link_libraries( 75 | flop_shaders 76 | INTERFACE ${SHADER_BIN}/$/flop_shaders_hex${CMAKE_STATIC_LIBRARY_SUFFIX}) 77 | target_include_directories( 78 | flop_shaders 79 | INTERFACE ${SHADER_BIN} 80 | ) 81 | add_dependencies(flop_shaders flop_shaders_hex_lib) 82 | -------------------------------------------------------------------------------- /shaders/CSFFilter.hlsl: -------------------------------------------------------------------------------- 1 | #include "Common.hlsli" 2 | 3 | // The CSF Gaussian blurs are done in two passes. First, we blur in the x direction, 4 | // then in the y direction (this choice is arbitrary since the Gaussian decomposition 5 | // is commutes). 6 | 7 | // These values are computed using the flip_kernels.js script 8 | static const float sy_kernel[] = { 9 | 0.39172750, 0.24189219, 0.05695543, 0.00511357, 0.00017506 10 | }; 11 | static const float sx_kernel[] = { 12 | 0.36889303, 0.24056897, 0.06672016, 0.00786960, 0.00039475 13 | }; 14 | static const float sz_kernel1[] = { 15 | 0.11730367, 0.11084383, 0.09352148, 0.07045487, 0.04739261, 0.02846493, 0.01526544, 0.00730985, 0.00312541, 0.00119318 16 | }; 17 | static const float sz_kernel2[] = { 18 | 0.08301017, 0.07581780, 0.05776847, 0.03671896, 0.01947017, 0.00861249, 0.00317810, 0.00097833, 0.00025124, 0.00005382 19 | }; 20 | 21 | #define KERNEL_RADIUS 9 22 | #define INNER_RADIUS 4 23 | 24 | #ifdef DIRECTION_X 25 | #define DIRECTION 0 26 | #endif 27 | 28 | #ifdef DIRECTION_Y 29 | #define DIRECTION 1 30 | #endif 31 | 32 | #ifndef DIRECTION 33 | #error "DIRECTION not specified. Specify DIRECTION=0 for a horizontal blur, and DIRECTION=1 for a vertical blur" 34 | #endif 35 | 36 | #define THREAD_COUNT 64 37 | #if THREAD_COUNT < 2 * KERNEL_RADIUS 38 | #error "THREAD_COUNT is too small for this implementation to work correctly" 39 | #endif 40 | 41 | struct PushConstants 42 | { 43 | uint2 extent; 44 | uint input; 45 | uint output; 46 | }; 47 | [[vk::push_constant]] 48 | PushConstants constants; 49 | 50 | [[vk::binding(1)]] 51 | RWTexture2D rwtextures[]; 52 | 53 | groupshared float4 data[KERNEL_RADIUS * 2 + THREAD_COUNT]; 54 | 55 | #if DIRECTION == 0 56 | [numthreads(THREAD_COUNT, 1, 1)] 57 | #else 58 | [numthreads(1, THREAD_COUNT, 1)] 59 | #endif 60 | void CSMain(uint3 id : SV_DispatchThreadID, int3 gtid : SV_GroupThreadID, int3 gid : SV_GroupID) 61 | { 62 | RWTexture2D input = rwtextures[constants.input]; 63 | RWTexture2D output = rwtextures[constants.output]; 64 | 65 | const uint lds_offset = gtid[DIRECTION] + KERNEL_RADIUS; 66 | 67 | // First, fetch all texture values needed starting with the central values 68 | int2 uv = clamp(id.xy, int2(0, 0), constants.extent - int2(1, 1)); 69 | #ifdef DIRECTION_X 70 | data[lds_offset] = input[uv].rgbb; 71 | #else 72 | data[lds_offset] = input[uv].rgba; 73 | #endif 74 | 75 | // Now, fetch the front and back of the window 76 | if (gtid[DIRECTION] < KERNEL_RADIUS * 2) 77 | { 78 | uint offset; 79 | if (gtid[DIRECTION] < KERNEL_RADIUS) 80 | { 81 | #if DIRECTION == 0 82 | uv = int2(gid.x * THREAD_COUNT, id.y); 83 | #else 84 | uv = int2(id.x, gid.y * THREAD_COUNT); 85 | #endif 86 | uv[DIRECTION] = uv[DIRECTION] - gtid[DIRECTION] - 1; 87 | offset = KERNEL_RADIUS - gtid[DIRECTION] - 1; 88 | } 89 | else 90 | { 91 | #if DIRECTION == 0 92 | uv = int2((gid.x + 1) * THREAD_COUNT, id.y); 93 | #else 94 | uv = int2(id.x, (gid.y + 1) * THREAD_COUNT); 95 | #endif 96 | uv[DIRECTION] = uv[DIRECTION] + gtid[DIRECTION] - KERNEL_RADIUS; 97 | offset = THREAD_COUNT + gtid[DIRECTION]; 98 | } 99 | 100 | uv = clamp(uv, int2(0, 0), constants.extent - int2(1, 1)); 101 | #if DIRECTION == 0 102 | data[offset] = input[uv].rgbb; 103 | #else 104 | data[offset] = input[uv].rgba; 105 | #endif 106 | } 107 | 108 | GroupMemoryBarrierWithGroupSync(); 109 | // At this point, all input values in our sliding window are in LDS and ready to use 110 | 111 | float4 color = data[lds_offset] * float4(sx_kernel[0], sy_kernel[0], sz_kernel1[0], sz_kernel2[0]); 112 | 113 | [unroll] 114 | for (int i = 1; i != INNER_RADIUS; ++i) 115 | { 116 | float2 xy = data[lds_offset - i].xy + data[lds_offset + i].xy; 117 | color.xy += float2(sy_kernel[i], sx_kernel[i]) * xy; 118 | } 119 | 120 | [unroll] 121 | for (int j = 1; j != KERNEL_RADIUS; ++j) 122 | { 123 | float2 zw = data[lds_offset - j].z + data[lds_offset + j].z; 124 | color.zw += float2(sz_kernel1[j], sz_kernel2[j]) * zw; 125 | } 126 | 127 | // Write out the result 128 | if (id.x < constants.extent.x && id.y < constants.extent.y) 129 | { 130 | #if DIRECTION == 0 131 | output[id.xy] = color; 132 | #else 133 | // Now that we've finished the blur passes, convert out of YyCxCz to xyz 134 | output[id.xy] = float4(linearized_Lab_to_xyz(float3(color.rg, color.z + color.w)), 1.0); 135 | #endif 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /shaders/ColorCompare.hlsl: -------------------------------------------------------------------------------- 1 | #include "Common.hlsli" 2 | 3 | struct PushConstants 4 | { 5 | uint2 extent; 6 | uint reference; 7 | uint test; 8 | uint output; 9 | }; 10 | [[vk::push_constant]] 11 | PushConstants constants; 12 | 13 | [[vk::binding(1)]] 14 | RWTexture2D rwtextures[]; 15 | 16 | void hunt_adjust(inout float3 Lab) 17 | { 18 | // Luminance is in the 0 to 100 range, so this scale factor is in [0, 1] 19 | float scale = 0.01 * Lab.x; 20 | 21 | // Dampen chrominance at lower luminance levels 22 | Lab.yz *= scale; 23 | } 24 | 25 | // Distance metrics for very large color differences 26 | // http://markfairchild.org/PDFs/PAP40.pdf 27 | // 28 | // Both inputs are expected to be in CIELAB space 29 | float HyAB_error(float3 r, float3 t) 30 | { 31 | float L_distance = abs(r.x - t.x); 32 | float a_delta = r.y - t.y; 33 | float b_delta = r.z - t.z; 34 | float ab_distance = sqrt(a_delta * a_delta + b_delta * b_delta); 35 | return L_distance + ab_distance; 36 | } 37 | 38 | // Max HyAB error is given by computing HyAB_error(green, blue)^0.7 offline 39 | static const float max_HyAB_error = 41.2760963; 40 | 41 | // When remapping HyAB error to [0, 1], FLIP linearly remaps values below this 42 | // cutoff to [0, 0.95). Values above this cutoff are linearly remapped to the 43 | // rest of the range. 44 | static const float cutoff = 0.4 * max_HyAB_error; 45 | static const float bias = 0.95; 46 | static const float cutoff_scale = bias / cutoff; 47 | 48 | // The FLIP paper notes that large differences ought to be compressed since the 49 | // distinction between white-black or green-blue are 3 times apart. 50 | float remap_HyAB_error(float error) 51 | { 52 | error = pow(error, 0.7); 53 | 54 | if (error < cutoff) 55 | { 56 | error *= cutoff_scale; 57 | } 58 | else 59 | { 60 | error = 0.05 * (error - cutoff) / (max_HyAB_error - cutoff) + bias; 61 | } 62 | return error; 63 | } 64 | 65 | [numthreads(8, 8, 1)] 66 | void CSMain(uint3 id : SV_DispatchThreadID) 67 | { 68 | if (id.x >= constants.extent[0] || id.y >= constants.extent[1]) 69 | { 70 | return; 71 | } 72 | 73 | // In the original flip paper, they apply an adjustment to the colors in 74 | // CIELAB space to account for the Hunt effect (chromatic differences are 75 | // more perceptually pronounced at higher luminance levels) 76 | 77 | RWTexture2D reference_image = rwtextures[constants.reference]; 78 | RWTexture2D test_image = rwtextures[constants.test]; 79 | 80 | float3 colors[2] = { reference_image[id.xy].rgb, test_image[id.xy].rgb }; 81 | 82 | colors[0] = xyz_to_CIELAB(colors[0]); 83 | colors[1] = xyz_to_CIELAB(colors[1]); 84 | 85 | hunt_adjust(colors[0]); 86 | hunt_adjust(colors[1]); 87 | 88 | float error = HyAB_error(colors[0], colors[1]); 89 | error = remap_HyAB_error(error); 90 | 91 | rwtextures[constants.output][id.xy].r = error; 92 | } 93 | -------------------------------------------------------------------------------- /shaders/Common.hlsli: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // RGB in this context refers to gamma-expanded sRGB values 4 | static const float3x3 rgb_to_xyz = 5 | float3x3( 6 | 0.4124564, 0.3575761, 0.1804375, 7 | 0.2126729, 0.7151522, 0.0721750, 8 | 0.0193339, 0.1191920, 0.9503041); 9 | 10 | static const float3x3 xyz_to_rgb = 11 | float3x3( 12 | 3.2404542, -1.5371385, -0.4985314, 13 | -0.969266, 1.8760108, 0.0415560, 14 | 0.0556434, -0.2040259, 1.0572252); 15 | 16 | // Note: The illuminant used here is normalized to 1 instead of 100 17 | static const float3 d65 = float3(0.950489, 1.0, 1.088840); 18 | static const float3 d65_rcp = 1.0 / d65; 19 | 20 | float3 xyz_to_linearized_Lab(float3 xyz) 21 | { 22 | float Yy = 116 * xyz.y - 16; 23 | float Cx = 500 * (xyz.x - xyz.y); 24 | float Cz = 200 * (xyz.y - xyz.z); 25 | return float3(Yy, Cx, Cz); 26 | } 27 | 28 | float3 rgb_to_linearized_Lab(float3 rgb) 29 | { 30 | float3 xyz = mul(rgb_to_xyz, rgb) * d65_rcp; 31 | return xyz_to_linearized_Lab(xyz); 32 | } 33 | 34 | float3 linearized_Lab_to_xyz(float3 YyCxCz) 35 | { 36 | float Yy = YyCxCz.x; 37 | float Cx = YyCxCz.y; 38 | float Cz = YyCxCz.z; 39 | float y = (Yy + 16) / 116; 40 | float x = y + Cx / 500; 41 | float z = y - Cz / 200; 42 | return float3(x, y, z) * d65; 43 | } 44 | 45 | float3 linearized_Lab_to_rgb(float3 YyCxCz) 46 | { 47 | float3 xyz = linearized_Lab_to_xyz(YyCxCz); 48 | return clamp(mul(xyz_to_rgb, xyz), 0, 1); 49 | } 50 | 51 | static const float delta = 0.2068966; 52 | static const float delta2 = delta * delta; 53 | static const float delta2_3 = 3.0 * delta2; 54 | static const float delta2_rcp3 = 1.0 / delta2_3; 55 | static const float delta3 = delta * delta2; 56 | static const float f_bias = 0.1379310; 57 | 58 | float CIELAB_f(float v) 59 | { 60 | if (v > delta3) 61 | { 62 | return pow(v, 0.3333333); 63 | } 64 | else 65 | { 66 | return delta2_rcp3 * v + f_bias; 67 | } 68 | } 69 | 70 | float3 xyz_to_CIELAB(float3 xyz) 71 | { 72 | float f_y = CIELAB_f(xyz.y); 73 | float L = 116 * f_y - 16; 74 | float a = 500 * (CIELAB_f(xyz.x) - f_y); 75 | float b = 200 * (f_y - CIELAB_f(xyz.z)); 76 | return float3(L, a, b); 77 | } 78 | 79 | float3 rgb_to_CIELAB(float3 rgb) 80 | { 81 | float3 xyz = mul(rgb_to_xyz, rgb) * d65_rcp; 82 | return xyz_to_CIELAB(xyz); 83 | } 84 | 85 | float CIELAB_f_inv(float v) 86 | { 87 | if (v > delta) 88 | { 89 | return pow(v, 3); 90 | } 91 | else 92 | { 93 | return delta2_3 * (v - f_bias); 94 | } 95 | } 96 | 97 | float3 CIELAB_to_rgb(float3 Lab) 98 | { 99 | float y_arg = (Lab.x + 16) / 116; 100 | float x = CIELAB_f_inv(y_arg + Lab.y / 500); 101 | float y = CIELAB_f_inv(y_arg); 102 | float z = CIELAB_f_inv(y_arg - Lab.z / 200); 103 | float3 xyz = d65 * float3(x, y, z); 104 | return clamp(mul(xyz_to_rgb, xyz), 0, 1); 105 | } 106 | 107 | 108 | // Various tonemapping operators 109 | 110 | static float const ACES[] = {0.6f * 0.6f * 2.51f, 0.6f * 0.03f, 0.6f * 0.6f * 2.43f, 0.6f * 0.59f, 0.14f}; 111 | 112 | float3 aces_tonemap(float3 c) 113 | { 114 | return saturate(((c * c) * ACES[0] + c * ACES[1]) / (c * c * ACES[2] + c * ACES[3] + ACES[4])); 115 | } 116 | 117 | float3 reinhard_tonemap(float3 c) 118 | { 119 | float lum = dot(float3(0.2126f, 0.7152f, 0.0722f), c); 120 | return saturate(c / (1.f + lum)); 121 | } 122 | 123 | static float const Hable[] = {0.231683f, 0.013791f, 0.18f, 0.3f, 0.018f}; 124 | 125 | float3 hable_tonemap(float3 c) 126 | { 127 | return saturate(((c * c) * Hable[0] + c * Hable[1]) / (c * c * Hable[2] + c * Hable[3] + Hable[4])); 128 | } 129 | -------------------------------------------------------------------------------- /shaders/ErrorColorMap.hlsl: -------------------------------------------------------------------------------- 1 | struct PushConstants 2 | { 3 | uint2 extent; 4 | float2 uv_offset; 5 | float uv_scale; 6 | uint input; 7 | uint color_map; 8 | }; 9 | [[vk::push_constant]] 10 | PushConstants constants; 11 | 12 | [[vk::binding(0)]] 13 | Texture2D textures[]; 14 | 15 | [[vk::binding(2)]] 16 | ByteAddressBuffer buffers[]; 17 | 18 | struct VSOutput 19 | { 20 | float4 position : SV_Position; 21 | float2 uv : TEXCOORD0; 22 | }; 23 | 24 | float4 PSMain(VSOutput IN) 25 | : SV_Target0 26 | { 27 | Texture2D error_image = textures[constants.input]; 28 | float error = error_image.Load(int3(IN.uv * constants.extent, 0)).r; 29 | float u = error * 255 + 0.5; 30 | uint left_index = clamp(floor(u), 0, 255); 31 | uint right_index = clamp(ceil(u), left_index, 255); 32 | // Interpolate between left and right endpoints 33 | float3 left = buffers[constants.color_map].Load(left_index * 12); 34 | float3 right = buffers[constants.color_map].Load(right_index * 12); 35 | 36 | return float4(lerp(left, right, frac(u)), 1.0); 37 | } 38 | -------------------------------------------------------------------------------- /shaders/FeatureFilter.hlsl: -------------------------------------------------------------------------------- 1 | // Edge and point filters are used to amplify color differences in the final error map 2 | 3 | // Gaussian 3-sigma kernel 4 | static const float kernel[] = { 5 | 0.14530192, 0.13598623, 0.11147196, 0.08003564, 0.05033249, 0.02772429, 0.01337580, 0.00565231, 0.00209209, 0.00067823 6 | }; 7 | // First-derivative (edge detector) 8 | // NOTE: When applying the left half of this kernel, the signs must be flipped 9 | static const float kernel1[] = { 10 | 0.00000000, -0.12572107, -0.20611460, -0.22198204, -0.18613221, -0.12815738, -0.07419661, -0.03657945, -0.01547329, -0.00564333 11 | }; 12 | // Second-derivative (point detector) 13 | static const float kernel2[] = { 14 | -0.29897641, -0.24272794, -0.10778385, 0.03217138, 0.11763449, 0.13377647, 0.10521736, 0.06477641, 0.03265116, 0.01377273 15 | }; 16 | 17 | #define KERNEL_RADIUS 9 18 | 19 | #ifdef DIRECTION_X 20 | #define DIRECTION 0 21 | #endif 22 | 23 | #ifdef DIRECTION_Y 24 | #define DIRECTION 1 25 | #endif 26 | 27 | #ifndef DIRECTION 28 | #error "DIRECTION not specified. Specify DIRECTION=0 for a horizontal blur, and DIRECTION=1 for a vertical blur" 29 | #endif 30 | 31 | #define THREAD_COUNT 64 32 | #if THREAD_COUNT < 2 * KERNEL_RADIUS 33 | #error "THREAD_COUNT is too small for this implementation to work correctly" 34 | #endif 35 | 36 | struct PushConstants 37 | { 38 | uint2 extent; 39 | // Input image expected to be in YyCxCz space (linearized CIELAB) 40 | uint input1; 41 | uint input2; 42 | uint output1; 43 | uint output2; 44 | }; 45 | [[vk::push_constant]] 46 | PushConstants constants; 47 | 48 | [[vk::binding(1)]] 49 | RWTexture2D rwtextures[]; 50 | 51 | #if DIRECTION == 0 52 | // In the initial horizontal pass, we filter the luminance component 53 | groupshared float data1[KERNEL_RADIUS * 2 + THREAD_COUNT]; 54 | groupshared float data2[KERNEL_RADIUS * 2 + THREAD_COUNT]; 55 | #else 56 | // In the vertical pass, we need to filter all three moments 57 | groupshared float3 data1[KERNEL_RADIUS * 2 + THREAD_COUNT]; 58 | groupshared float3 data2[KERNEL_RADIUS * 2 + THREAD_COUNT]; 59 | #endif 60 | 61 | // Normalize luminance to [0, 1] 62 | float normalize_Yy(float Yy) 63 | { 64 | static const float scale = 1.0 / 116.0; 65 | static const float bias = 16.0 / 116.0; 66 | 67 | return Yy * scale + bias; 68 | } 69 | 70 | #if DIRECTION == 0 71 | [numthreads(THREAD_COUNT, 1, 1)] 72 | #else 73 | [numthreads(1, THREAD_COUNT, 1)] 74 | #endif 75 | void CSMain(uint3 id : SV_DispatchThreadID, int3 gtid : SV_GroupThreadID, int3 gid : SV_GroupID) 76 | { 77 | RWTexture2D input1 = rwtextures[constants.input1]; 78 | RWTexture2D input2 = rwtextures[constants.input2]; 79 | 80 | const uint lds_offset = gtid[DIRECTION] + KERNEL_RADIUS; 81 | 82 | // First, fetch all texture values needed starting with the central values 83 | int2 uv = clamp(id.xy, int2(0, 0), constants.extent - int2(1, 1)); 84 | 85 | #if DIRECTION == 0 86 | data1[lds_offset] = normalize_Yy(input1[uv].r); 87 | data2[lds_offset] = normalize_Yy(input2[uv].r); 88 | #else 89 | data1[lds_offset] = input1[uv].rgb; 90 | data2[lds_offset] = input2[uv].rgb; 91 | #endif 92 | 93 | // Now, fetch the front and back of the window 94 | if (gtid[DIRECTION] < KERNEL_RADIUS * 2) 95 | { 96 | uint offset; 97 | if (gtid[DIRECTION] < KERNEL_RADIUS) 98 | { 99 | #if DIRECTION == 0 100 | uv = int2(gid.x * THREAD_COUNT, id.y); 101 | #else 102 | uv = int2(id.x, gid.y * THREAD_COUNT); 103 | #endif 104 | uv[DIRECTION] = uv[DIRECTION] - gtid[DIRECTION] - 1; 105 | offset = KERNEL_RADIUS - gtid[DIRECTION] - 1; 106 | } 107 | else 108 | { 109 | #if DIRECTION == 0 110 | uv = int2((gid.x + 1) * THREAD_COUNT, id.y); 111 | #else 112 | uv = int2(id.x, (gid.y + 1) * THREAD_COUNT); 113 | #endif 114 | uv[DIRECTION] = uv[DIRECTION] + gtid[DIRECTION] - KERNEL_RADIUS; 115 | offset = THREAD_COUNT + gtid[DIRECTION]; 116 | } 117 | 118 | uv = clamp(uv, int2(0, 0), constants.extent - int2(1, 1)); 119 | #if DIRECTION == 0 120 | data1[offset] = normalize_Yy(input1[uv].r); 121 | data2[offset] = normalize_Yy(input2[uv].r); 122 | #else 123 | data1[offset] = input1[uv].rgb; 124 | data2[offset] = input2[uv].rgb; 125 | #endif 126 | } 127 | 128 | GroupMemoryBarrierWithGroupSync(); 129 | // At this point, all input values in our sliding window are in LDS and ready to use 130 | 131 | #if DIRECTION == 0 132 | float3 moments; 133 | moments.xz = data1[lds_offset] * float2(kernel[0], kernel[2]); 134 | moments.y = 0.0; 135 | 136 | for (int i = 1; i != KERNEL_RADIUS; ++i) 137 | { 138 | float left = data1[lds_offset - i]; 139 | float right = data1[lds_offset + i]; 140 | moments.xz += (left + right) * float2(kernel[i], kernel2[i]); 141 | moments.y += kernel1[i] * (right - left); 142 | } 143 | 144 | if (id.x < constants.extent.x && id.y < constants.extent.y) 145 | { 146 | rwtextures[constants.output1][id.xy].rgb = moments; 147 | } 148 | 149 | moments.xz = data2[lds_offset] * float2(kernel[0], kernel[2]); 150 | moments.y = 0.0; 151 | 152 | for (int j = 1; j != KERNEL_RADIUS; ++j) 153 | { 154 | float left = data2[lds_offset - j]; 155 | float right = data2[lds_offset + j]; 156 | moments.xz += (left + right) * float2(kernel[j], kernel2[j]); 157 | moments.y += kernel1[j] * (right - left); 158 | } 159 | 160 | if (id.x < constants.extent.x && id.y < constants.extent.y) 161 | { 162 | rwtextures[constants.output2][id.xy].rgb = moments; 163 | } 164 | #else 165 | // Find filtered x and y derivatives, with edges in compoment 0, points in component 1 166 | // Intuitively, we have the Gaussian-blurred luminance in the x direction, so we need to 167 | // apply the edge and point filters to that quantity. For the y direction, we have the 168 | // edge and point filtered luminance values, so we convolve those quantities with the 169 | // Gaussian. 170 | float2 features1_x = kernel[0] * data1[lds_offset].yz; 171 | float2 features1_y = float2(kernel[1], kernel[2]) * data1[lds_offset].x; 172 | 173 | for (int i = 1; i != KERNEL_RADIUS; ++i) 174 | { 175 | float3 left = data1[lds_offset - i]; 176 | float3 right = data1[lds_offset + i]; 177 | features1_x += kernel[i] * (left.yz + right.yz); 178 | features1_y.y += kernel2[i] * (left.x + right.x); 179 | features1_y.x += kernel1[i] * (right.x - left.x); 180 | } 181 | 182 | float2 features1 = sqrt(features1_x * features1_x + features1_y * features1_y); 183 | 184 | float2 features2_x = kernel[0] * data2[lds_offset].yz; 185 | float2 features2_y = float2(kernel[1], kernel[2]) * data2[lds_offset].x; 186 | 187 | for (int j = 1; j != KERNEL_RADIUS; ++j) 188 | { 189 | float3 left = data2[lds_offset - j]; 190 | float3 right = data2[lds_offset + j]; 191 | features2_x += kernel[j] * (left.yz + right.yz); 192 | features2_y.y += kernel2[j] * (left.x + right.x); 193 | features2_y.x += kernel1[j] * (right.x - left.x); 194 | } 195 | 196 | float2 features2 = sqrt(features2_x * features2_x + features2_y * features2_y); 197 | 198 | if (id.x < constants.extent.x && id.y < constants.extent.y) 199 | { 200 | RWTexture2D output = rwtextures[constants.output1]; 201 | 202 | // We can now compare features and use differences in edges and points detected to 203 | // amplify the color error 204 | 205 | float2 feature_delta = abs(features2 - features1); 206 | // TODO: make 0.5 configurable to amplify or dampen error due to feature differences 207 | float feature_error = pow(max(feature_delta.x, feature_delta.y) / sqrt(2), 0.5); 208 | 209 | float color_error = output[id.xy].r; 210 | 211 | // The moment we've all been waiting for 212 | float flip_error = pow(color_error, 1.0 - feature_error); 213 | 214 | output[id.xy].rgb = flip_error; 215 | } 216 | #endif 217 | } 218 | -------------------------------------------------------------------------------- /shaders/FullscreenVS.hlsl: -------------------------------------------------------------------------------- 1 | struct PushConstants 2 | { 3 | uint2 extent; 4 | float2 uv_offset; 5 | float uv_scale; 6 | }; 7 | [[vk::push_constant]] 8 | PushConstants constants; 9 | 10 | struct VSOutput 11 | { 12 | float4 position : SV_Position; 13 | float2 uv : TEXCOORD0; 14 | }; 15 | 16 | VSOutput VSMain(uint id : SV_VertexID) 17 | { 18 | VSOutput OUT; 19 | 20 | // 0 -> (-1, -1, 0.5, 1) 21 | // 1 -> ( 3, -1, 0.5, 1) 22 | // 2 -> (-1, 3, 0.5, 1) 23 | OUT.position.x = id == 1 ? 3.0 : -1.0; 24 | OUT.position.y = id == 2 ? 3.0 : -1.0; 25 | OUT.position.zw = float2(0.0, 1.0); 26 | OUT.uv = OUT.position.xy * 0.5 + 0.5; 27 | OUT.uv = constants.uv_scale * OUT.uv + constants.uv_offset; 28 | 29 | return OUT; 30 | } 31 | -------------------------------------------------------------------------------- /shaders/HexToLib.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | project(FlopShaders) 3 | 4 | message(STATUS "Compiling the following spirv hex files: @FLOP_SPIRV_HEX@") 5 | 6 | add_library( 7 | flop_shaders_hex 8 | STATIC 9 | ${FLOP_SPIRV_HEX}) 10 | -------------------------------------------------------------------------------- /shaders/Preview.hlsl: -------------------------------------------------------------------------------- 1 | #include "Common.hlsli" 2 | 3 | struct PushConstants 4 | { 5 | float2 uv_offset; 6 | float uv_scale; 7 | 8 | uint input; 9 | uint color_map; 10 | uint tonemap; 11 | float exposure; 12 | }; 13 | [[vk::push_constant]] 14 | PushConstants constants; 15 | 16 | [[vk::binding(0)]] 17 | Texture2D textures[]; 18 | 19 | [[vk::binding(2)]] 20 | ByteAddressBuffer buffers[]; 21 | 22 | [[vk::binding(3)]] 23 | SamplerState texture_sampler; 24 | 25 | struct VSOutput 26 | { 27 | float4 position : SV_Position; 28 | float2 uv : TEXCOORD0; 29 | }; 30 | 31 | VSOutput VSMain(uint id : SV_VertexID) 32 | { 33 | VSOutput OUT; 34 | 35 | // 0 -> (-1, -1, 0.5, 1) 36 | // 1 -> ( 3, -1, 0.5, 1) 37 | // 2 -> (-1, 3, 0.5, 1) 38 | OUT.position.x = id == 1 ? 3.0 : -1.0; 39 | OUT.position.y = id == 2 ? 3.0 : -1.0; 40 | OUT.position.zw = float2(0.0, 1.0); 41 | OUT.uv = OUT.position.xy * 0.5 + 0.5; 42 | OUT.uv = constants.uv_scale * OUT.uv + constants.uv_offset; 43 | 44 | return OUT; 45 | } 46 | 47 | struct PSOutput 48 | { 49 | float4 color : SV_Target0; 50 | }; 51 | 52 | PSOutput PSMain(VSOutput IN) 53 | { 54 | PSOutput OUT; 55 | 56 | #ifdef COLORMAP 57 | if (IN.uv.x > 1.0 || IN.uv.y > 1.0 || IN.uv.x < 0.0 || IN.uv.y < 0.0) 58 | { 59 | OUT.color = float4(0.0, 0.0, 0.0, 1.0); 60 | return OUT; 61 | } 62 | 63 | float r = textures[constants.input].Sample(texture_sampler, IN.uv).r; 64 | float u = r * (255) + 0.5; 65 | uint left_index = clamp(floor(u), 0, 255); 66 | uint right_index = clamp(ceil(u), left_index, 255); 67 | // Interpolate between left and right endpoints 68 | float3 left = buffers[constants.color_map].Load(left_index * 12); 69 | float3 right = buffers[constants.color_map].Load(right_index * 12); 70 | OUT.color = float4(lerp(left, right, frac(u)), 1.0); 71 | #else 72 | float4 color = textures[constants.input].Sample(texture_sampler, IN.uv); 73 | 74 | if (constants.tonemap == 1) 75 | { 76 | color.rgb = aces_tonemap(constants.exposure * color.rgb); 77 | } 78 | else if (constants.tonemap == 2) 79 | { 80 | color.rgb = reinhard_tonemap(constants.exposure * color.rgb); 81 | } 82 | else if (constants.tonemap == 3) 83 | { 84 | color.rgb = hable_tonemap(constants.exposure * color.rgb); 85 | } 86 | 87 | OUT.color = color; 88 | #endif 89 | return OUT; 90 | } 91 | -------------------------------------------------------------------------------- /shaders/Summarize.hlsl: -------------------------------------------------------------------------------- 1 | // Reduction kernel to summarize stats in a histogram 2 | 3 | struct PushConstants 4 | { 5 | uint2 extent; 6 | uint input; 7 | uint output; 8 | }; 9 | [[vk::push_constant]] 10 | PushConstants constants; 11 | 12 | [[vk::binding(1)]] 13 | RWTexture2D rwtextures[]; 14 | 15 | [[vk::binding(2)]] 16 | RWByteAddressBuffer rwbuffers[]; 17 | 18 | // Construct an LDS histogram with 32 entries 19 | #define BUCKET_COUNT 32 20 | groupshared uint histogram[BUCKET_COUNT]; 21 | 22 | [numthreads(8, 8, 1)] 23 | void CSMain(uint3 id : SV_DispatchThreadID, uint3 gtid : SV_GroupThreadID) 24 | { 25 | uint linear_gtid = gtid.x * 8 + gtid.y; 26 | if (linear_gtid < BUCKET_COUNT) 27 | { 28 | histogram[linear_gtid] = 0; 29 | } 30 | 31 | GroupMemoryBarrierWithGroupSync(); 32 | 33 | if (id.x < constants.extent[0] && id.y < constants.extent[1]) 34 | { 35 | RWTexture2D error_texture = rwtextures[constants.input]; 36 | 37 | float error = clamp(error_texture[id.xy].r, 0.0, 1.0); 38 | 39 | InterlockedAdd(histogram[floor(error * (BUCKET_COUNT - 1))], 1); 40 | } 41 | 42 | GroupMemoryBarrierWithGroupSync(); 43 | 44 | if (linear_gtid < BUCKET_COUNT) 45 | { 46 | rwbuffers[constants.output].InterlockedAdd(linear_gtid * 4, histogram[linear_gtid]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /shaders/Tonemap.hlsl: -------------------------------------------------------------------------------- 1 | #include "Common.hlsli" 2 | 3 | // Assumes the input and output image dimensions are the same 4 | struct PushConstants 5 | { 6 | uint2 extent; 7 | float2 uv_offset; 8 | float uv_scale; 9 | uint input; 10 | float exposure; 11 | }; 12 | 13 | [[vk::push_constant]] 14 | PushConstants constants; 15 | 16 | [[vk::binding(0)]] 17 | Texture2D textures[]; 18 | 19 | struct VSOutput 20 | { 21 | float4 position : SV_Position; 22 | float2 uv : TEXCOORD0; 23 | }; 24 | 25 | float4 PSMain(VSOutput IN) : SV_Target0 26 | { 27 | Texture2D input_texture = textures[constants.input]; 28 | 29 | float4 color = input_texture.Load(int3(IN.uv * constants.extent, 0)); 30 | 31 | color.rgb = aces_tonemap(constants.exposure * color.rgb); 32 | 33 | return color; 34 | } 35 | -------------------------------------------------------------------------------- /shaders/YyCxCz.hlsl: -------------------------------------------------------------------------------- 1 | #include "Common.hlsli" 2 | 3 | // Convert linearized RGB to YyCxCz 4 | // https://engineering.purdue.edu/~bouman/software/YCxCz/pdf/ColorFidelityMetrics.pdf 5 | // https://engineering.purdue.edu/~bouman/publications/pdf/ei93.pdf 6 | // http://users.ece.utexas.edu/~bevans/papers/2003/colorHalftoning/colorHVSspl00282.pdf 7 | 8 | // Assumes the input and output image dimensions are the same 9 | struct PushConstants 10 | { 11 | uint2 extent; 12 | float2 uv_offset; 13 | float uv_scale; 14 | uint input; 15 | uint tonemap; 16 | float exposure; 17 | // If 1, multiply Yy component by alpha to account for alpha differences 18 | uint handle_alpha; 19 | }; 20 | 21 | [[vk::push_constant]] 22 | PushConstants constants; 23 | 24 | [[vk::binding(0)]] 25 | Texture2D textures[]; 26 | 27 | struct VSOutput 28 | { 29 | float4 position : SV_Position; 30 | float2 uv : TEXCOORD0; 31 | }; 32 | 33 | float4 PSMain(VSOutput IN) : SV_Target0 34 | { 35 | Texture2D input_texture = textures[constants.input]; 36 | float4 color = input_texture.Load(int3(IN.uv * constants.extent, 0)); 37 | 38 | if (constants.tonemap == 1) 39 | { 40 | color.rgb = aces_tonemap(constants.exposure * color.rgb); 41 | } 42 | else if (constants.tonemap == 2) 43 | { 44 | color.rgb = reinhard_tonemap(constants.exposure * color.rgb); 45 | } 46 | else if (constants.tonemap == 3) 47 | { 48 | color.rgb = hable_tonemap(constants.exposure * color.rgb); 49 | } 50 | 51 | float3 YyCxCz = rgb_to_linearized_Lab(color.rgb); 52 | 53 | if (constants.handle_alpha != 0) 54 | { 55 | YyCxCz.x *= color.a; 56 | } 57 | 58 | return float4(YyCxCz, 1.f); 59 | } 60 | -------------------------------------------------------------------------------- /shaders/flip_kernels.js: -------------------------------------------------------------------------------- 1 | // This script was used to compute constants used in the algorithm offline, 2 | // as described in the paper: https://research.nvidia.com/publication/2020-07_FLIP 3 | 4 | // PPD (pixels per degree) below assumes a 0.709x0.399 m^2 monitor at 4k resolution (3840x2160) 5 | // at a distance of 0.70 meters. Because the aspect ratio is assumed to be 1:1, we can restrict 6 | // the formula to just one dimension 7 | 8 | function ppd(length, resolution, distance) 9 | { 10 | return distance * resolution / length * Math.PI / 180; 11 | } 12 | // Dimensions of a 32" 4k TV/Monitor 13 | const ppd_x = ppd(0.709, 3840, 0.7); 14 | const ppd_y = ppd(0.399, 2160, 0.7); 15 | console.log('Pixels per degree (x): ', ppd_x); 16 | console.log('Pixels per degree (y): ', ppd_y); 17 | 18 | // The original paper rounds this quantity down to 67 19 | const p = Math.ceil((ppd_x + ppd_y) / 2); 20 | const spacing = 1 / p; 21 | console.log('Spacing between two samples: ', spacing); 22 | 23 | // CSFs are approximated using 4 gaussians. 1 each for the luminance and red-green CSFs, 24 | // and 2 for the blue-yellow CSF 25 | 26 | // Gaussian: 27 | // g(x) = a * sqrt(pi / b) * exp(-pi^2 / b * x^2) 28 | 29 | // The filters are normalized later so a isn't needed for S_y and S_x. 30 | // S_y: a -> ?, b -> 0.0047 31 | // S_x: a -> ?, b -> 0.0053 32 | // S_z: a1 -> 34.1, b1 -> 0.04, a2 -> 13.5, b2 -> 0.025 33 | 34 | const b_Sy = 0.0047; 35 | const b_Sx = 0.0053; 36 | const a1_Sz = 34.1; 37 | const a2_Sz = 13.5; 38 | const b1_Sz = 0.04; 39 | const b2_Sz = 0.025; 40 | 41 | // The filter radius for all 3 CSFs is the same in the paper, which they choose to be the 42 | // radius corresponding to the CSF with the widest spread (S_z with b at 0.04). This is 43 | // pretty inefficient it turns out, given that the filter radii needed for Sy and Sx are 44 | // less than half the size of the filter radius for Sz. Furthermore, the second gaussian 45 | // needed to fit the Sz CSF is slightly smaller as well. 46 | 47 | function filter_radius(b) 48 | { 49 | const sigma = Math.sqrt(b / 2 / (Math.PI * Math.PI)); 50 | return Math.ceil(sigma * 3 * p); 51 | } 52 | 53 | // Compute filter values for a pixel-centered box kernel for (x, y) in 54 | // {0, +/- 1, +/- 2, ... +/- r} 55 | // Don't bother doing the full box since we'll be performing the filter in two passes 56 | function filter_weights(a, b, radius_override) 57 | { 58 | const r = radius_override || filter_radius(b); 59 | const weights = new Array(r + 1); 60 | for (let i = 0; i < r + 1; ++i) 61 | { 62 | const d = spacing * i; 63 | const d2 = d * d; 64 | weights[i] = a * Math.sqrt(Math.PI / b) * Math.exp(-Math.PI * Math.PI / b * d2); 65 | } 66 | return [weights, r]; 67 | } 68 | 69 | function filter_weights_sqrt(a, b, radius_override) 70 | { 71 | const r = radius_override || filter_radius(b); 72 | const weights = new Array(r + 1); 73 | for (let i = 0; i < r + 1; ++i) 74 | { 75 | const d = spacing * i; 76 | const d2 = d * d; 77 | weights[i] = Math.sqrt(a * Math.sqrt(Math.PI / b)) * Math.exp(-Math.PI * Math.PI / b * d2); 78 | } 79 | return [weights, r]; 80 | } 81 | 82 | const [weights_Sy, r_Sy] = filter_weights(1, b_Sy); 83 | const [weights_Sx, r_Sx] = filter_weights(1, b_Sx); 84 | const [weights1_Sz, r1_Sz] = filter_weights_sqrt(a1_Sz, b1_Sz); 85 | const [weights2_Sz, r2_Sz] = filter_weights_sqrt(a2_Sz, b2_Sz, filter_radius(b1_Sz)); 86 | 87 | console.log(`Filter radii (r_Sy, r_Sx, r_Sz): ${r_Sy}, ${r_Sx}, ${r1_Sz}`); 88 | 89 | function sum_weights(weights) 90 | { 91 | let sum = 0; 92 | for (let i = 1; i != weights.length; ++i) 93 | { 94 | sum += weights[i]; 95 | } 96 | // Double both sides, then add the center-point 97 | sum *= 2; 98 | sum += weights[0]; 99 | return sum; 100 | } 101 | 102 | const norm_Sy = 1 / sum_weights(weights_Sy); 103 | const norm_Sx = 1 / sum_weights(weights_Sx); 104 | const norm_Sz1 = sum_weights(weights1_Sz); 105 | const norm_Sz2 = sum_weights(weights2_Sz); 106 | console.log(norm_Sz1, norm_Sz2); 107 | const norm_Sz = 1 / Math.sqrt(norm_Sz1 * norm_Sz1 + norm_Sz2 * norm_Sz2); 108 | console.log(`Norm factors: ${norm_Sy}, ${norm_Sx}, ${norm_Sz}`); 109 | 110 | // The weights need to be normalized such that the sum of all weights sum to one 111 | function normalize_weights(weights, factor) 112 | { 113 | for (let i = 0; i != weights.length; ++i) 114 | { 115 | weights[i] = weights[i] * factor; 116 | } 117 | } 118 | 119 | normalize_weights(weights_Sy, norm_Sy); 120 | console.log(`Sy: ${weights_Sy.map(v => v.toFixed(8)).join(', ')}`); 121 | 122 | normalize_weights(weights_Sx, norm_Sx); 123 | console.log(`Sx: ${weights_Sx.map(v => v.toFixed(8)).join(', ')}`); 124 | 125 | normalize_weights(weights1_Sz, norm_Sz); 126 | normalize_weights(weights2_Sz, norm_Sz); 127 | console.log(`Sz: ${weights1_Sz.map(v => v.toFixed(8)).join(', ')}`); 128 | console.log(`Sz: ${weights2_Sz.map(v => v.toFixed(8)).join(', ')}`); 129 | 130 | // From "Estimates of edge detection filters in human vision" 131 | // https://www.sciencedirect.com/science/article/pii/S0042698918302050 132 | // HVS width from highest to lowest amplitude is 0.082 degrees 133 | const feature_std_dev = 0.5 * 0.082 * 67; 134 | console.log(`Feature std dev: ${feature_std_dev}`); 135 | const feature_kernel_radius = Math.ceil(feature_std_dev * 3); // 9 pixels 136 | console.log(`Feature kernel radius: ${feature_kernel_radius}`); 137 | 138 | // Compute weights for the first and second partial derivatives of a Gaussian kernel 139 | 140 | function feature_weights() 141 | { 142 | // Three weight arrays for the 0th, 1st, and 2nd moments 143 | const weights = [ 144 | new Array(feature_kernel_radius + 1), 145 | new Array(feature_kernel_radius + 1), 146 | new Array(feature_kernel_radius + 1), 147 | ]; 148 | let b = 0.5 / feature_std_dev / feature_std_dev; 149 | 150 | let sum = 0; 151 | let sum_dx = 0; 152 | // For the second partial derivatives, compute the weight sums separately 153 | let sum_ddx = [0, 0]; 154 | for (let i = 0; i != weights[0].length; ++i) 155 | { 156 | weights[0][i] = Math.exp(-i * i * b); 157 | sum += weights[0][i]; 158 | 159 | // First derivative 160 | weights[1][i] = -i * weights[0][i]; 161 | if (weights[1][i] > 0) 162 | { 163 | sum_dx += weights[1][i]; 164 | } 165 | else 166 | { 167 | sum_dx -= weights[1][i]; 168 | } 169 | 170 | // Second derivative 171 | weights[2][i] = (i * i * b * 2 - 1) * weights[0][i]; 172 | if (weights[2][i] > 0) 173 | { 174 | sum_ddx[0] += weights[2][i]; 175 | } 176 | else 177 | { 178 | sum_ddx[1] -= weights[2][i]; 179 | } 180 | } 181 | 182 | // Fix the sums to account for axial symmetry 183 | sum = 2 * (sum - weights[0][0]) + weights[0][0]; 184 | 185 | // The first derivative kernel is mirrored across the origin and are accounted 186 | // for already by accumulating both positive and negative weights into a single 187 | // quantity. 188 | 189 | console.log(sum_ddx); 190 | console.log(weights[2][0]) 191 | // The second derivative is symmetric about the origin 192 | if (weights[2][0] > 0) 193 | { 194 | sum_ddx[0] = 2 * (sum_ddx[0] - weights[2][0]) + weights[2][0]; 195 | sum_ddx[1] *= 2; 196 | } 197 | else 198 | { 199 | sum_ddx[0] *= 2; 200 | sum_ddx[1] = 2 * (sum_ddx[1] + weights[2][0]) - weights[2][0]; 201 | } 202 | 203 | console.log(`Feature sums: ${sum}, ${sum_dx}, ${sum_ddx[0]}:${sum_ddx[1]}`); 204 | 205 | // Normalize weights 206 | for (let i = 0; i != weights[0].length; ++i) 207 | { 208 | weights[0][i] /= sum; 209 | weights[1][i] /= sum_dx; 210 | if (weights[2][i] > 0) 211 | { 212 | weights[2][i] /= sum_ddx[0]; 213 | } 214 | else 215 | { 216 | weights[2][i] /= sum_ddx[1]; 217 | } 218 | } 219 | return weights; 220 | } 221 | 222 | const feature_kernel_weights = feature_weights(); 223 | console.log(`${feature_kernel_weights[0].map(v => v.toFixed(8)).join(', ')}`); 224 | console.log(`${feature_kernel_weights[1].map(v => v.toFixed(8)).join(', ')}`); 225 | console.log(`${feature_kernel_weights[2].map(v => v.toFixed(8)).join(', ')}`); 226 | -------------------------------------------------------------------------------- /shaders/imgui.ini: -------------------------------------------------------------------------------- 1 | [Window][Debug##Default] 2 | Pos=60,60 3 | Size=400,400 4 | Collapsed=0 5 | 6 | [Window][Dear ImGui Demo] 7 | Pos=1402,424 8 | Size=550,680 9 | Collapsed=0 10 | 11 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # External dependencies 2 | 3 | set(CLI11_BUILD_DOCS OFF CACHE BOOL "") 4 | set(CLI11_BUILD_TESTS OFF CACHE BOOL "") 5 | set(CLI11_BUILD_EXAMPLES OFF CACHE BOOL "") 6 | set(CLI11_BUILD_EXAMPLES_JSON OFF CACHE BOOL "") 7 | set(CLI11_INSTALL OFF CACHE BOOL "") 8 | FetchContent_Declare( 9 | cli11 10 | GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git 11 | GIT_TAG v2.1.2 12 | GIT_SHALLOW ON 13 | ) 14 | 15 | set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "") 16 | set(GLFW_BUILD_TESTS OFF CACHE BOOL "") 17 | set(GLFW_BUILD_DOCS OFF CACHE BOOL "") 18 | set(GLFW_INSTALL OFF CACHE BOOL "") 19 | FetchContent_Declare( 20 | glfw 21 | URL https://github.com/glfw/glfw/releases/download/3.3.6/glfw-3.3.6.zip 22 | ) 23 | 24 | FetchContent_Declare( 25 | nfd 26 | GIT_REPOSITORY https://github.com/btzy/nativefiledialog-extended.git 27 | GIT_TAG 28ade5a5cc5d17cea8fe4034572cac8fd54eb53f 28 | GIT_SHALLOW ON 29 | ) 30 | 31 | FetchContent_MakeAvailable(cli11 glfw nfd) 32 | 33 | FetchContent_Declare( 34 | imgui 35 | GIT_REPOSITORY https://github.com/ocornut/imgui 36 | GIT_TAG v1.86 37 | GIT_SHALLOW ON 38 | ) 39 | if(NOT imgui_POPULATED) 40 | FetchContent_Populate(imgui) 41 | endif() 42 | 43 | add_library( 44 | limgui 45 | ${imgui_SOURCE_DIR}/imgui.cpp 46 | ${imgui_SOURCE_DIR}/imgui_draw.cpp 47 | ${imgui_SOURCE_DIR}/imgui_tables.cpp 48 | ${imgui_SOURCE_DIR}/imgui_widgets.cpp 49 | ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp 50 | ) 51 | target_compile_features( 52 | limgui 53 | PRIVATE 54 | cxx_std_17 55 | ) 56 | target_include_directories(limgui 57 | PUBLIC ${imgui_SOURCE_DIR} 58 | PRIVATE ${imgui_SOURCE_DIR}/backends) 59 | target_link_libraries(limgui PUBLIC glfw volk) 60 | target_compile_definitions(limgui PUBLIC IMGUI_IMPL_VULKAN_NO_PROTOTYPES VK_NO_PROTOTYPES) 61 | 62 | add_executable(flop) 63 | 64 | target_sources(flop 65 | PUBLIC 66 | Main.cpp 67 | ImGuiVulkan.cpp 68 | Preview.cpp 69 | Preview.hpp 70 | UI.cpp 71 | UI.hpp 72 | ) 73 | 74 | target_include_directories( 75 | flop 76 | PUBLIC 77 | ${CMAKE_CURRENT_SOURCE_DIR}/../lib 78 | ) 79 | 80 | target_compile_features( 81 | flop 82 | PUBLIC 83 | cxx_std_20 84 | ) 85 | 86 | target_compile_definitions( 87 | flop 88 | PUBLIC 89 | NOMINMAX=1 90 | ) 91 | 92 | target_link_libraries( 93 | flop 94 | PUBLIC 95 | CLI11::CLI11 96 | glfw 97 | lflop 98 | limgui 99 | nfd 100 | ) 101 | -------------------------------------------------------------------------------- /src/ImguiVulkan.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | -------------------------------------------------------------------------------- /src/Main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "Preview.hpp" 8 | #include "UI.hpp" 9 | 10 | #define GLFW_INCLUDE_VULKAN 11 | #include 12 | #undef APIENTRY 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | static VkSurfaceKHR s_surface; 24 | static VkSurfaceFormatKHR s_surface_format; 25 | static GLFWwindow* s_window; 26 | static VkSwapchainKHR s_swapchain; 27 | static VkRenderPass s_render_pass; 28 | static ImGui_ImplVulkanH_Window s_imgui_window; 29 | static bool s_swapchain_broken = false; 30 | static UI s_ui; 31 | 32 | using namespace flop; 33 | 34 | void on_resize(GLFWwindow* window, int width, int height) 35 | { 36 | s_swapchain_broken = true; 37 | } 38 | 39 | PFN_vkVoidFunction load_vulkan_function(char const* function, void*) 40 | { 41 | return vkGetInstanceProcAddr(g_instance, function); 42 | } 43 | 44 | double ref_width() 45 | { 46 | return g_reference.source_.width_; 47 | } 48 | 49 | double ref_height() 50 | { 51 | return g_reference.source_.height_; 52 | } 53 | 54 | void on_scroll(GLFWwindow* window, double x_offset, double y_offset) 55 | { 56 | bool magnify = y_offset > 0; 57 | s_ui.on_scroll(window, magnify); 58 | 59 | ImGui_ImplGlfw_ScrollCallback(window, x_offset, y_offset); 60 | } 61 | 62 | void on_mouse_click(GLFWwindow* window, int button, int action, int mods) 63 | { 64 | s_ui.on_click(window, button, action); 65 | } 66 | 67 | void set_pan_zoom_callbacks() 68 | { 69 | glfwSetScrollCallback(s_window, on_scroll); 70 | glfwSetMouseButtonCallback(s_window, on_mouse_click); 71 | } 72 | 73 | void init_frames() 74 | { 75 | s_imgui_window.Frames = new ImGui_ImplVulkanH_Frame[2]{}; 76 | s_imgui_window.FrameSemaphores = new ImGui_ImplVulkanH_FrameSemaphores[2]{}; 77 | 78 | for (uint32_t i = 0; i != s_imgui_window.ImageCount; ++i) 79 | { 80 | ImGui_ImplVulkanH_Frame& frame = s_imgui_window.Frames[i]; 81 | VkFenceCreateInfo fence_info{.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, 82 | .flags = VK_FENCE_CREATE_SIGNALED_BIT}; 83 | vkCreateFence(g_device, &fence_info, nullptr, &frame.Fence); 84 | 85 | VkSemaphoreCreateInfo semaphore_info{ 86 | .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; 87 | vkCreateSemaphore( 88 | g_device, 89 | &semaphore_info, 90 | nullptr, 91 | &s_imgui_window.FrameSemaphores[i].ImageAcquiredSemaphore); 92 | vkCreateSemaphore( 93 | g_device, 94 | &semaphore_info, 95 | nullptr, 96 | &s_imgui_window.FrameSemaphores[i].RenderCompleteSemaphore); 97 | } 98 | } 99 | 100 | void init_frame_command_buffers() 101 | { 102 | for (uint32_t i = 0; i != s_imgui_window.ImageCount; ++i) 103 | { 104 | ImGui_ImplVulkanH_Frame& frame = s_imgui_window.Frames[i]; 105 | 106 | if (frame.CommandPool) 107 | { 108 | vkFreeCommandBuffers( 109 | g_device, frame.CommandPool, 1, &frame.CommandBuffer); 110 | vkDestroyCommandPool(g_device, frame.CommandPool, nullptr); 111 | } 112 | 113 | VkCommandPoolCreateInfo command_pool_info{ 114 | .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, 115 | .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, 116 | .queueFamilyIndex = g_graphics_queue_index, 117 | }; 118 | vkCreateCommandPool( 119 | g_device, &command_pool_info, nullptr, &frame.CommandPool); 120 | 121 | VkCommandBufferAllocateInfo command_buffer_info{ 122 | .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, 123 | .commandPool = frame.CommandPool, 124 | .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, 125 | .commandBufferCount = 1}; 126 | vkAllocateCommandBuffers( 127 | g_device, &command_buffer_info, &frame.CommandBuffer); 128 | } 129 | } 130 | 131 | void create_swapchain(); 132 | 133 | void render() 134 | { 135 | VkSemaphore acquire_surface 136 | = s_imgui_window.FrameSemaphores[s_imgui_window.SemaphoreIndex] 137 | .ImageAcquiredSemaphore; 138 | VkSemaphore release_surface 139 | = s_imgui_window.FrameSemaphores[s_imgui_window.SemaphoreIndex] 140 | .RenderCompleteSemaphore; 141 | VkResult error = vkAcquireNextImageKHR(g_device, 142 | s_imgui_window.Swapchain, 143 | UINT64_MAX, 144 | acquire_surface, 145 | VK_NULL_HANDLE, 146 | &s_imgui_window.FrameIndex); 147 | if (error == VK_ERROR_OUT_OF_DATE_KHR || error == VK_SUBOPTIMAL_KHR) 148 | { 149 | s_swapchain_broken = true; 150 | return; 151 | } 152 | 153 | ImGui_ImplVulkanH_Frame& frame 154 | = s_imgui_window.Frames[s_imgui_window.FrameIndex]; 155 | vkWaitForFences(g_device, 1, &frame.Fence, VK_TRUE, UINT64_MAX); 156 | vkResetFences(g_device, 1, &frame.Fence); 157 | 158 | vkResetCommandPool(g_device, frame.CommandPool, 0); 159 | VkCommandBufferBeginInfo begin 160 | = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, 161 | .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; 162 | vkBeginCommandBuffer(frame.CommandBuffer, &begin); 163 | 164 | // Blit reference image into position 165 | 166 | VkImageSubresourceRange transfer_range{.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, 167 | .baseMipLevel = 0, 168 | .levelCount = 1, 169 | .baseArrayLayer = 0, 170 | .layerCount = 1}; 171 | VkImageMemoryBarrier dst_transfer{ 172 | .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 173 | .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT, 174 | .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, 175 | .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, 176 | .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 177 | .srcQueueFamilyIndex = g_graphics_queue_index, 178 | .dstQueueFamilyIndex = g_graphics_queue_index, 179 | .image = frame.Backbuffer, 180 | .subresourceRange = transfer_range}; 181 | vkCmdPipelineBarrier(frame.CommandBuffer, 182 | VK_PIPELINE_STAGE_TRANSFER_BIT, 183 | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 184 | 0, 185 | 0, 186 | nullptr, 187 | 0, 188 | nullptr, 189 | 1, 190 | &dst_transfer); 191 | 192 | VkClearValue clear_value{.color = {.int32 = {0, 0, 0, 0}}}; 193 | VkRenderPassBeginInfo render_pass_begin{ 194 | .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, 195 | .renderPass = s_imgui_window.RenderPass, 196 | .framebuffer = frame.Framebuffer, 197 | .renderArea 198 | = {.offset = {.x = 0, .y = 0}, 199 | .extent = {.width = static_cast(s_imgui_window.Width), 200 | .height = static_cast(s_imgui_window.Height)}}, 201 | .clearValueCount = 1, 202 | .pClearValues = &clear_value}; 203 | vkCmdBeginRenderPass( 204 | frame.CommandBuffer, &render_pass_begin, VK_SUBPASS_CONTENTS_INLINE); 205 | 206 | s_ui.render(s_window, frame.CommandBuffer); 207 | 208 | vkCmdEndRenderPass(frame.CommandBuffer); 209 | 210 | VkPipelineStageFlags wait_stage 211 | = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; 212 | VkSubmitInfo submit{.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, 213 | .waitSemaphoreCount = 1, 214 | .pWaitSemaphores = &acquire_surface, 215 | .pWaitDstStageMask = &wait_stage, 216 | .commandBufferCount = 1, 217 | .pCommandBuffers = &frame.CommandBuffer, 218 | .signalSemaphoreCount = 1, 219 | .pSignalSemaphores = &release_surface}; 220 | vkEndCommandBuffer(frame.CommandBuffer); 221 | vkQueueSubmit(g_graphics_queue, 1, &submit, frame.Fence); 222 | } 223 | 224 | void present() 225 | { 226 | if (s_swapchain_broken) 227 | { 228 | return; 229 | } 230 | 231 | VkSemaphore release_surface 232 | = s_imgui_window.FrameSemaphores[s_imgui_window.SemaphoreIndex] 233 | .RenderCompleteSemaphore; 234 | VkPresentInfoKHR present{.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, 235 | .waitSemaphoreCount = 1, 236 | .pWaitSemaphores = &release_surface, 237 | .swapchainCount = 1, 238 | .pSwapchains = &s_imgui_window.Swapchain, 239 | .pImageIndices = &s_imgui_window.FrameIndex}; 240 | VkResult error = vkQueuePresentKHR(g_graphics_queue, &present); 241 | 242 | if (error == VK_ERROR_OUT_OF_DATE_KHR || error == VK_SUBOPTIMAL_KHR) 243 | { 244 | s_swapchain_broken = true; 245 | return; 246 | } 247 | 248 | s_imgui_window.SemaphoreIndex 249 | = (s_imgui_window.SemaphoreIndex + 1) % s_imgui_window.ImageCount; 250 | } 251 | 252 | static void on_drop(GLFWwindow* window, int path_count, const char* paths[]) 253 | { 254 | if (path_count == 1) 255 | { 256 | // Determine whether we've dragged onto the reference widget, or the 257 | // test widget. 258 | } 259 | } 260 | 261 | void init_render_pass(); 262 | 263 | int main(int argc, char const* argv[]) 264 | { 265 | CLI::App app{ 266 | "An image comparison tool and visualizer. All options are optional " 267 | "unless headless mode is requested.", 268 | "FLOP"}; 269 | std::string reference; 270 | app.add_option("-r,--reference", reference, "Path to reference image"); 271 | std::string test; 272 | app.add_option("-t,--test", test, "Path to test image"); 273 | std::string output; 274 | app.add_option("-o,--output", output, "Path to output file."); 275 | 276 | float exposure = 1.f; 277 | app.add_option("-e,--exposure", 278 | exposure, 279 | "Exposure to apply to an HDR image (log 2 stops)"); 280 | 281 | std::unordered_map tonemappers{ 282 | {"ACES", Tonemap::ACES}, 283 | {"Reinhard", Tonemap::Reinhard}, 284 | {"Hable", Tonemap::Hable}}; 285 | Tonemap tonemap = Tonemap::ACES; 286 | app.add_option("--tonemapper", tonemap, "HDR to LDR tonemapping operator") 287 | ->transform(CLI::CheckedTransformer(tonemappers, CLI::ignore_case)); 288 | 289 | int headless; 290 | app.add_flag( 291 | "--hl,--headless", headless, "Request that a gui not be presented"); 292 | 293 | int force; 294 | app.add_flag("-f,--force", 295 | force, 296 | "Overwrite image if file exists at specified output path"); 297 | 298 | CLI11_PARSE(app, argc, argv); 299 | 300 | if (!output.empty()) 301 | { 302 | std::error_code ec; 303 | auto stat = std::filesystem::status(output, ec); 304 | if (!ec) 305 | { 306 | switch (stat.type()) 307 | { 308 | case std::filesystem::file_type::none: 309 | case std::filesystem::file_type::not_found: 310 | break; 311 | case std::filesystem::file_type::directory: 312 | std::printf( 313 | "Error: Output path supplied (%s) is a directory.\n", 314 | output.c_str()); 315 | return 1; 316 | case std::filesystem::file_type::symlink: 317 | case std::filesystem::file_type::block: 318 | case std::filesystem::file_type::character: 319 | case std::filesystem::file_type::fifo: 320 | case std::filesystem::file_type::socket: 321 | case std::filesystem::file_type::regular: 322 | case std::filesystem::file_type::unknown: 323 | default: 324 | if (force == 0) 325 | { 326 | std::printf( 327 | "Error: File exists at output path %s. Pass -f or " 328 | "--force to overwrite it.\n", 329 | output.c_str()); 330 | return 1; 331 | } 332 | break; 333 | } 334 | } 335 | } 336 | 337 | s_ui.set_reference(reference); 338 | s_ui.set_test(test); 339 | s_ui.set_output(output); 340 | s_ui.set_tonemap(tonemap); 341 | s_ui.set_exposure(exposure); 342 | 343 | if (!glfwInit()) 344 | { 345 | std::cerr << "Failed to initialize GLFW!\n"; 346 | return 1; 347 | } 348 | 349 | if (headless == 0) 350 | { 351 | NFD_Init(); 352 | 353 | glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); 354 | s_window = glfwCreateWindow(1920, 1080, "FLOP", nullptr, nullptr); 355 | 356 | if (!glfwVulkanSupported()) 357 | { 358 | std::cout << "Vulkan not supported!\n"; 359 | return 1; 360 | } 361 | 362 | glfwSetDropCallback(s_window, on_drop); 363 | glfwSetFramebufferSizeCallback(s_window, on_resize); 364 | 365 | uint32_t extCount; 366 | char const** exts = glfwGetRequiredInstanceExtensions(&extCount); 367 | flop_init(extCount, exts); 368 | } 369 | else 370 | { 371 | flop_init(0, nullptr); 372 | } 373 | 374 | if (!(reference.empty() || test.empty())) 375 | { 376 | if (s_ui.analyze(false)) 377 | { 378 | } 379 | } 380 | 381 | if (headless > 0) 382 | { 383 | return 0; 384 | } 385 | 386 | if (glfwCreateWindowSurface(g_instance, s_window, nullptr, &s_surface) 387 | != VK_SUCCESS) 388 | { 389 | std::cout << "Failed to create Vulkan window surface!\n"; 390 | return 1; 391 | } 392 | 393 | std::vector surface_formats 394 | = vk_enumerate( 395 | vkGetPhysicalDeviceSurfaceFormatsKHR, g_physical_device, s_surface); 396 | 397 | bool surface_found = false; 398 | for (VkSurfaceFormatKHR const& format : surface_formats) 399 | { 400 | // TODO: Rec.2020/HDR-swapchain support 401 | if (format.format == VK_FORMAT_B8G8R8_SRGB) 402 | { 403 | s_surface_format = format; 404 | surface_found = true; 405 | break; 406 | } 407 | } 408 | if (!surface_found) 409 | { 410 | s_surface_format = surface_formats[0]; 411 | } 412 | 413 | // We're required to validate the surface is supported before use, or the 414 | // validation layers will complain 415 | VkBool32 surface_supported; 416 | vkGetPhysicalDeviceSurfaceSupportKHR( 417 | g_physical_device, g_graphics_queue_index, s_surface, &surface_supported); 418 | if (!surface_supported) 419 | { 420 | std::cout << "Surface not supported.\n"; 421 | return 1; 422 | } 423 | 424 | init_render_pass(); 425 | Preview::init(s_render_pass); 426 | 427 | IMGUI_CHECKVERSION(); 428 | 429 | ImGui::CreateContext(); 430 | ImGui::StyleColorsDark(); 431 | 432 | ImGui_ImplGlfw_InitForVulkan(s_window, true); 433 | set_pan_zoom_callbacks(); 434 | 435 | ImGui_ImplVulkan_LoadFunctions(load_vulkan_function); 436 | 437 | int width; 438 | int height; 439 | glfwGetFramebufferSize(s_window, &width, &height); 440 | s_imgui_window.Surface = s_surface; 441 | s_imgui_window.SurfaceFormat = s_surface_format; 442 | s_imgui_window.PresentMode = VK_PRESENT_MODE_FIFO_KHR; 443 | s_imgui_window.ClearEnable = true; 444 | s_imgui_window.RenderPass = s_render_pass; 445 | s_imgui_window.ImageCount = 2; 446 | 447 | ImGui_ImplVulkan_InitInfo imgui_vulkan_info{ 448 | .Instance = g_instance, 449 | .PhysicalDevice = g_physical_device, 450 | .Device = g_device, 451 | .QueueFamily = g_graphics_queue_index, 452 | .Queue = g_graphics_queue, 453 | .PipelineCache = VK_NULL_HANDLE, 454 | .DescriptorPool = g_descriptor_pool, 455 | .Subpass = 0, 456 | .MinImageCount = 2, 457 | .ImageCount = 2, 458 | .MSAASamples = VK_SAMPLE_COUNT_1_BIT, 459 | .Allocator = nullptr, 460 | .CheckVkResultFn = nullptr, 461 | }; 462 | ImGui_ImplVulkan_Init(&imgui_vulkan_info, s_render_pass); 463 | ImGui_ImplVulkan_SetMinImageCount(2); 464 | init_frames(); 465 | create_swapchain(); 466 | 467 | VkCommandBuffer cb = g_command_buffers[3]; 468 | VkCommandBufferBeginInfo begin{ 469 | .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, 470 | .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; 471 | vkBeginCommandBuffer(cb, &begin); 472 | ImGui_ImplVulkan_CreateFontsTexture(cb); 473 | vkEndCommandBuffer(cb); 474 | VkSubmitInfo submit{.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, 475 | .commandBufferCount = 1, 476 | .pCommandBuffers = &cb}; 477 | vkQueueSubmit(g_graphics_queue, 1, &submit, VK_NULL_HANDLE); 478 | vkDeviceWaitIdle(g_device); 479 | ImGui_ImplVulkan_DestroyFontUploadObjects(); 480 | 481 | int image_focus = 1; 482 | while (!glfwWindowShouldClose(s_window)) 483 | { 484 | // glfwWaitEventsTimeout(1); 485 | glfwWaitEvents(); 486 | 487 | if (s_swapchain_broken) 488 | { 489 | int width; 490 | int height; 491 | glfwGetFramebufferSize(s_window, &width, &height); 492 | if (width > 0 && height > 0) 493 | { 494 | create_swapchain(); 495 | s_imgui_window.FrameIndex = 0; 496 | s_swapchain_broken = false; 497 | s_ui.mark_viewport_dirty(); 498 | } 499 | } 500 | 501 | ImGui_ImplVulkan_NewFrame(); 502 | ImGui_ImplGlfw_NewFrame(); 503 | 504 | s_ui.update(); 505 | 506 | ImGui::Render(); 507 | 508 | ImDrawData& draw_data = *ImGui::GetDrawData(); 509 | if (draw_data.DisplaySize.x > 0.f && draw_data.DisplaySize.y > 0.f) 510 | { 511 | render(); 512 | present(); 513 | } 514 | } 515 | 516 | return 0; 517 | } 518 | 519 | void init_render_pass() 520 | { 521 | // Create a single color target, single subpass render pass. 522 | VkAttachmentDescription color_attachment{ 523 | .format = s_surface_format.format, 524 | .samples = VK_SAMPLE_COUNT_1_BIT, 525 | .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, 526 | .storeOp = VK_ATTACHMENT_STORE_OP_STORE, 527 | .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, 528 | .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, 529 | .initialLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 530 | .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, 531 | }; 532 | VkAttachmentReference color_attachment_reference{ 533 | .attachment = 0, .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; 534 | VkSubpassDescription subpass{ 535 | .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, 536 | .colorAttachmentCount = 1, 537 | .pColorAttachments = &color_attachment_reference, 538 | .pDepthStencilAttachment = nullptr}; 539 | VkSubpassDependency subpass_dependency{ 540 | .srcSubpass = VK_SUBPASS_EXTERNAL, 541 | .dstSubpass = 0, 542 | .srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT, 543 | .dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 544 | | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT, 545 | .srcAccessMask = 0, 546 | .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT}; 547 | 548 | VkRenderPassCreateInfo render_pass_info{ 549 | .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, 550 | .attachmentCount = 1, 551 | .pAttachments = &color_attachment, 552 | .subpassCount = 1, 553 | .pSubpasses = &subpass, 554 | .dependencyCount = 1, 555 | .pDependencies = &subpass_dependency}; 556 | if (vkCreateRenderPass(g_device, &render_pass_info, nullptr, &s_render_pass) 557 | != VK_SUCCESS) 558 | { 559 | std::cout << "Failed to create Vulkan render pass!\n"; 560 | abort(); 561 | } 562 | } 563 | 564 | void create_swapchain() 565 | { 566 | VkSwapchainKHR old_swapchain = s_imgui_window.Swapchain; 567 | s_imgui_window.Swapchain = VK_NULL_HANDLE; 568 | 569 | vkDeviceWaitIdle(g_device); 570 | 571 | init_frame_command_buffers(); 572 | 573 | for (uint32_t i = 0; i != s_imgui_window.ImageCount; ++i) 574 | { 575 | if (s_imgui_window.Frames[i].BackbufferView) 576 | { 577 | vkDestroyImageView( 578 | g_device, s_imgui_window.Frames[i].BackbufferView, nullptr); 579 | } 580 | if (s_imgui_window.Frames[i].Framebuffer) 581 | { 582 | vkDestroyFramebuffer( 583 | g_device, s_imgui_window.Frames[i].Framebuffer, nullptr); 584 | } 585 | } 586 | 587 | if (s_imgui_window.Pipeline) 588 | { 589 | vkDestroyPipeline(g_device, s_imgui_window.Pipeline, nullptr); 590 | } 591 | 592 | VkSwapchainCreateInfoKHR swapchain_info{ 593 | .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, 594 | .surface = s_imgui_window.Surface, 595 | .minImageCount = 2, 596 | .imageFormat = s_imgui_window.SurfaceFormat.format, 597 | .imageColorSpace = s_imgui_window.SurfaceFormat.colorSpace, 598 | .imageArrayLayers = 1, 599 | .imageUsage 600 | = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, 601 | .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, 602 | .preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR, 603 | .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, 604 | .presentMode = s_imgui_window.PresentMode, 605 | .clipped = VK_TRUE, 606 | .oldSwapchain = old_swapchain, 607 | }; 608 | VkSurfaceCapabilitiesKHR surface_capabilities; 609 | vkGetPhysicalDeviceSurfaceCapabilitiesKHR( 610 | g_physical_device, s_imgui_window.Surface, &surface_capabilities); 611 | 612 | if (surface_capabilities.minImageCount < 2) 613 | { 614 | swapchain_info.minImageCount = surface_capabilities.minImageCount; 615 | } 616 | 617 | int width; 618 | int height; 619 | glfwGetFramebufferSize(s_window, &width, &height); 620 | if (surface_capabilities.currentExtent.width == 0xffffffff) 621 | { 622 | s_imgui_window.Width = width; 623 | swapchain_info.imageExtent.width = width; 624 | s_imgui_window.Height = height; 625 | swapchain_info.imageExtent.height = height; 626 | } 627 | else 628 | { 629 | s_imgui_window.Width = surface_capabilities.currentExtent.width; 630 | swapchain_info.imageExtent.width 631 | = surface_capabilities.currentExtent.width; 632 | s_imgui_window.Height = surface_capabilities.currentExtent.height; 633 | swapchain_info.imageExtent.height 634 | = surface_capabilities.currentExtent.height; 635 | } 636 | 637 | vkCreateSwapchainKHR( 638 | g_device, &swapchain_info, nullptr, &s_imgui_window.Swapchain); 639 | vkGetSwapchainImagesKHR( 640 | g_device, s_imgui_window.Swapchain, &s_imgui_window.ImageCount, nullptr); 641 | 642 | VkImage backbuffers[2]; 643 | vkGetSwapchainImagesKHR(g_device, 644 | s_imgui_window.Swapchain, 645 | &s_imgui_window.ImageCount, 646 | backbuffers); 647 | 648 | for (uint32_t i = 0; i != s_imgui_window.ImageCount; ++i) 649 | { 650 | s_imgui_window.Frames[i].Backbuffer = backbuffers[i]; 651 | } 652 | 653 | if (old_swapchain != VK_NULL_HANDLE) 654 | { 655 | vkDestroySwapchainKHR(g_device, old_swapchain, nullptr); 656 | } 657 | 658 | VkImageViewCreateInfo view_info{ 659 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, 660 | .viewType = VK_IMAGE_VIEW_TYPE_2D, 661 | .format = s_imgui_window.SurfaceFormat.format, 662 | .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, 663 | }; 664 | for (uint32_t i = 0; i != s_imgui_window.ImageCount; ++i) 665 | { 666 | view_info.image = s_imgui_window.Frames[i].Backbuffer; 667 | vkCreateImageView(g_device, 668 | &view_info, 669 | nullptr, 670 | &s_imgui_window.Frames[i].BackbufferView); 671 | } 672 | 673 | VkImageView framebuffer_attachment; 674 | VkFramebufferCreateInfo framebuffer_info{ 675 | .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, 676 | .renderPass = s_render_pass, 677 | .attachmentCount = 1, 678 | .pAttachments = &framebuffer_attachment, 679 | .width = static_cast(s_imgui_window.Width), 680 | .height = static_cast(s_imgui_window.Height), 681 | .layers = 1}; 682 | for (uint32_t i = 0; i != s_imgui_window.ImageCount; ++i) 683 | { 684 | framebuffer_attachment = s_imgui_window.Frames[i].BackbufferView; 685 | vkCreateFramebuffer(g_device, 686 | &framebuffer_info, 687 | nullptr, 688 | &s_imgui_window.Frames[i].Framebuffer); 689 | } 690 | } 691 | -------------------------------------------------------------------------------- /src/Preview.cpp: -------------------------------------------------------------------------------- 1 | #include "Preview.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | Preview* previews[4]; 14 | 15 | static VkPipeline s_pipeline; 16 | static VkPipeline s_pipeline_color_map; 17 | static VkPipelineLayout s_pipeline_layout; 18 | 19 | struct PushConstants 20 | { 21 | float uv_offset[2]; 22 | float uv_scale; 23 | uint32_t input; 24 | uint32_t color_map; 25 | uint32_t tonemap; 26 | float exposure; 27 | }; 28 | using namespace flop; 29 | 30 | void Preview::init(VkRenderPass render_pass) 31 | { 32 | VkPushConstantRange push_constant_range{ 33 | .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 34 | .offset = 0, 35 | .size = sizeof(PushConstants)}; 36 | VkPipelineLayoutCreateInfo layout_info{ 37 | .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, 38 | .setLayoutCount = 1, 39 | .pSetLayouts = &g_descriptor_set_layout, 40 | .pushConstantRangeCount = 1, 41 | .pPushConstantRanges = &push_constant_range}; 42 | vkCreatePipelineLayout(g_device, &layout_info, nullptr, &s_pipeline_layout); 43 | 44 | VkShaderModule vs_shader 45 | = Kernel::compile_shader(PreviewVS_spv_data, PreviewVS_spv_size); 46 | VkShaderModule ps_shader 47 | = Kernel::compile_shader(PreviewPS_spv_data, PreviewPS_spv_size); 48 | VkPipelineShaderStageCreateInfo stages[2] = { 49 | { 50 | .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, 51 | .stage = VK_SHADER_STAGE_VERTEX_BIT, 52 | .module = vs_shader, 53 | .pName = "VSMain", 54 | }, 55 | { 56 | .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, 57 | .stage = VK_SHADER_STAGE_FRAGMENT_BIT, 58 | .module = ps_shader, 59 | .pName = "PSMain", 60 | }, 61 | }; 62 | 63 | VkPipelineVertexInputStateCreateInfo vi{ 64 | .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, 65 | .vertexBindingDescriptionCount = 0, 66 | .pVertexBindingDescriptions = nullptr, 67 | .vertexAttributeDescriptionCount = 0, 68 | .pVertexAttributeDescriptions = nullptr, 69 | }; 70 | 71 | VkPipelineInputAssemblyStateCreateInfo ia{ 72 | .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, 73 | .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}; 74 | VkPipelineViewportStateCreateInfo v{ 75 | .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, 76 | .viewportCount = 1, 77 | .pViewports = nullptr, 78 | .scissorCount = 1, 79 | .pScissors = nullptr}; 80 | VkPipelineRasterizationStateCreateInfo rs{ 81 | .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, 82 | .depthClampEnable = false, 83 | .rasterizerDiscardEnable = false, 84 | .polygonMode = VK_POLYGON_MODE_FILL, 85 | .cullMode = VK_CULL_MODE_NONE, 86 | .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, 87 | .lineWidth = 1.f}; 88 | VkPipelineMultisampleStateCreateInfo ms{ 89 | .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, 90 | .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, 91 | .sampleShadingEnable = VK_FALSE, 92 | .pSampleMask = nullptr, 93 | .alphaToCoverageEnable = VK_FALSE, 94 | .alphaToOneEnable = VK_FALSE}; 95 | VkPipelineDepthStencilStateCreateInfo ds{ 96 | .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; 97 | VkPipelineColorBlendAttachmentState attachment{ 98 | .blendEnable = VK_FALSE, 99 | .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT 100 | | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT}; 101 | VkPipelineColorBlendStateCreateInfo cbs{ 102 | .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, 103 | .attachmentCount = 1, 104 | .pAttachments = &attachment}; 105 | VkDynamicState dynamic_states[] 106 | = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; 107 | VkPipelineDynamicStateCreateInfo dyn{ 108 | .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, 109 | .dynamicStateCount = 2, 110 | .pDynamicStates = dynamic_states}; 111 | 112 | VkGraphicsPipelineCreateInfo pipeline_info{ 113 | .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, 114 | .stageCount = 2, 115 | .pStages = stages, 116 | .pVertexInputState = &vi, 117 | .pInputAssemblyState = &ia, 118 | .pTessellationState = nullptr, 119 | .pViewportState = &v, 120 | .pRasterizationState = &rs, 121 | .pMultisampleState = &ms, 122 | .pDepthStencilState = &ds, 123 | .pColorBlendState = &cbs, 124 | .pDynamicState = &dyn, 125 | .layout = s_pipeline_layout, 126 | .renderPass = render_pass, 127 | .subpass = 0, 128 | .basePipelineHandle = VK_NULL_HANDLE, 129 | .basePipelineIndex = 0, 130 | }; 131 | vkCreateGraphicsPipelines( 132 | g_device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &s_pipeline); 133 | 134 | vkDestroyShaderModule(g_device, ps_shader, nullptr); 135 | ps_shader = Kernel::compile_shader( 136 | PreviewPSColorMap_spv_data, PreviewPSColorMap_spv_size); 137 | stages[1].module = ps_shader; 138 | 139 | vkCreateGraphicsPipelines( 140 | g_device, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &s_pipeline_color_map); 141 | 142 | vkDestroyShaderModule(g_device, vs_shader, nullptr); 143 | vkDestroyShaderModule(g_device, ps_shader, nullptr); 144 | } 145 | 146 | void Preview::reset_viewport() 147 | { 148 | uv_offset_[0] = 0; 149 | uv_offset_[1] = 0; 150 | old_uv_offset_[0] = 0; 151 | old_uv_offset_[1] = 0; 152 | set_viewport(viewport_); 153 | } 154 | 155 | void Preview::set_exposure(float exposure) 156 | { 157 | exposure_ = exposure; 158 | } 159 | 160 | void Preview::set_quadrant(Quadrant quadrant) 161 | { 162 | quadrant_ = quadrant; 163 | } 164 | 165 | void Preview::set_tonemap(Tonemap tonemap) 166 | { 167 | tonemap_ = tonemap; 168 | } 169 | 170 | void Preview::set_image(Image& image) 171 | { 172 | image_ = ℑ 173 | } 174 | 175 | void Preview::set_viewport(Rect2D viewport) 176 | { 177 | viewport_ = viewport; 178 | if (!image_ || !image_->image_) 179 | { 180 | viewport_dirty_ = true; 181 | return; 182 | } 183 | 184 | if (image_->aspect() < preview_viewport_.width / preview_viewport_.height) 185 | { 186 | preview_viewport_.height = viewport.y2 - viewport.y1; 187 | preview_viewport_.width = preview_viewport_.height / image_->aspect(); 188 | } 189 | else 190 | { 191 | preview_viewport_.width = viewport.x2 - viewport.x1; 192 | preview_viewport_.height = preview_viewport_.width / image_->aspect(); 193 | } 194 | preview_viewport_.minDepth = 0.f; 195 | preview_viewport_.maxDepth = 1.f; 196 | 197 | preview_scissor_.extent.width = 0.5f * (viewport.x2 - viewport.x1); 198 | preview_scissor_.extent.height = 0.5f * (viewport.y2 - viewport.y1); 199 | 200 | if (image_->aspect() < preview_viewport_.width / preview_viewport_.height) 201 | { 202 | uv_scale_ = preview_viewport_.width / preview_scissor_.extent.width; 203 | } 204 | else 205 | { 206 | uv_scale_ = preview_viewport_.height / preview_scissor_.extent.height; 207 | } 208 | 209 | switch (quadrant_) 210 | { 211 | using enum Quadrant; 212 | case TopLeft: 213 | preview_viewport_.x = viewport.x1; 214 | preview_viewport_.y = viewport.y1; 215 | break; 216 | case TopRight: 217 | preview_viewport_.x = (viewport.x2 - viewport.x1) * 0.5f; 218 | preview_viewport_.y = viewport.y1; 219 | break; 220 | case BottomRight: 221 | preview_viewport_.x = (viewport.x2 - viewport.x1) * 0.5f; 222 | preview_viewport_.y = (viewport.y2 - viewport.y1) * 0.5f; 223 | break; 224 | case BottomLeft: 225 | preview_viewport_.x = viewport.x1; 226 | preview_viewport_.y = (viewport.y2 - viewport.y1) * 0.5f; 227 | break; 228 | default: 229 | break; 230 | } 231 | 232 | preview_scissor_.offset.x = preview_viewport_.x; 233 | preview_scissor_.offset.y = preview_viewport_.y; 234 | } 235 | 236 | void Preview::set_color_map(ColorMap color_map) 237 | { 238 | Buffer& buffer = get_color_map(color_map); 239 | color_map_ = buffer.index_; 240 | assert(buffer.size_ / sizeof(float) / 3 == 256); 241 | use_color_map_ = true; 242 | } 243 | 244 | void Preview::viewport_extent(int& vw, int& vh) const 245 | { 246 | vw = viewport_.x2 - viewport_.x1; 247 | vh = viewport_.y2 - viewport_.y1; 248 | } 249 | 250 | float Preview::viewport_aspect() const 251 | { 252 | int vw; 253 | int vh; 254 | viewport_extent(vw, vh); 255 | return static_cast(vw) / vh; 256 | } 257 | 258 | float Preview::max_scale() const 259 | { 260 | // The maximum scale corresponds to the scale factor such that a pixel in 261 | // the source image is stretched out to encompass the width or height of the 262 | // quadrant viewport (whichever is smaller) 263 | 264 | int vw; 265 | int vh; 266 | viewport_extent(vw, vh); 267 | 268 | return 0.5f * std::min(vw, vh); 269 | } 270 | 271 | float Preview::min_scale() const 272 | { 273 | // The minimum scale is the smallest scale that constrains the image to the 274 | // quadrant 275 | float vaspect = viewport_aspect(); 276 | float aspect = image_->aspect(); 277 | 278 | if (aspect < vaspect) 279 | { 280 | return static_cast(viewport_.y2 - viewport_.y1) / image_->height_ 281 | * 0.5f; 282 | } 283 | 284 | return static_cast(viewport_.x2 - viewport_.x1) / image_->width_ 285 | * 0.5f; 286 | } 287 | 288 | void Preview::render(GLFWwindow* window, VkCommandBuffer cb) 289 | { 290 | if (image_ == nullptr || image_->image_ == VK_NULL_HANDLE || viewport_.x2 == 0) 291 | { 292 | return; 293 | } 294 | 295 | if (viewport_dirty_) 296 | { 297 | set_viewport(viewport_); 298 | viewport_dirty_ = false; 299 | } 300 | 301 | if (pan_active_) 302 | { 303 | double cx; 304 | double cy; 305 | glfwGetCursorPos(window, &cx, &cy); 306 | 307 | double delta_x = cx - cursor_[0]; 308 | double delta_y = cy - cursor_[1]; 309 | 310 | uv_offset_[0] 311 | = old_uv_offset_[0] - delta_x * uv_scale_ / preview_viewport_.width; 312 | uv_offset_[1] 313 | = old_uv_offset_[1] - delta_y * uv_scale_ / preview_viewport_.height; 314 | } 315 | 316 | PushConstants push_constants{ 317 | .uv_offset = {uv_offset_[0], uv_offset_[1]}, 318 | .uv_scale = uv_scale_, 319 | .input = image_->index_, 320 | .color_map = color_map_, 321 | .tonemap = image_->hdr_ ? static_cast(tonemap_) : 0u, 322 | .exposure = exposure_}; 323 | vkCmdPushConstants(cb, 324 | s_pipeline_layout, 325 | VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 326 | 0, 327 | sizeof(PushConstants), 328 | &push_constants); 329 | 330 | vkCmdSetViewport(cb, 0, 1, &preview_viewport_); 331 | 332 | vkCmdSetScissor(cb, 0, 1, &preview_scissor_); 333 | 334 | vkCmdBindDescriptorSets(cb, 335 | VK_PIPELINE_BIND_POINT_GRAPHICS, 336 | s_pipeline_layout, 337 | 0, 338 | 1, 339 | &g_descriptor_set, 340 | 0, 341 | nullptr); 342 | 343 | if (use_color_map_) 344 | { 345 | vkCmdBindPipeline( 346 | cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pipeline_color_map); 347 | } 348 | else 349 | { 350 | vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, s_pipeline); 351 | } 352 | 353 | vkCmdDraw(cb, 3, 1, 0, 0); 354 | } 355 | 356 | void Preview::zoom(GLFWwindow* window, bool magnify) 357 | { 358 | if (image_ == nullptr || viewport_.x2 == 0) 359 | { 360 | // The preview needs to render at least once with default magnification 361 | // to discover the viewport 362 | return; 363 | } 364 | 365 | // Determine the fixed point based on cursor position 366 | double c_x; 367 | double c_y; 368 | glfwGetCursorPos(window, &c_x, &c_y); 369 | 370 | // Quit if the mouse is in the upper right quadrant 371 | if (c_x > viewport_.x2 / 2 && c_y < viewport_.y2 / 2) 372 | { 373 | return; 374 | } 375 | 376 | // Magnify and minify in 5% stops 377 | if (magnify) 378 | { 379 | uv_scale_ -= 0.05f; 380 | uv_scale_ = std::max(uv_scale_, 0.05f); 381 | } 382 | else 383 | { 384 | uv_scale_ += 0.05f; 385 | uv_scale_ = std::min(uv_scale_, 10.f); 386 | } 387 | } 388 | 389 | void Preview::activate_pan(GLFWwindow* window) 390 | { 391 | if (image_ == nullptr || viewport_.x2 == 0) 392 | { 393 | // The preview needs to render at least once with default magnification 394 | // to discover the viewport 395 | return; 396 | } 397 | 398 | // Determine the fixed point based on cursor position 399 | glfwGetCursorPos(window, cursor_, cursor_ + 1); 400 | 401 | // Quit if the mouse is in the upper right quadrant 402 | if (cursor_[0] > viewport_.x2 / 2 && cursor_[1] < viewport_.y2 / 2) 403 | { 404 | return; 405 | } 406 | 407 | pan_active_ = true; 408 | old_uv_offset_[0] = uv_offset_[0]; 409 | old_uv_offset_[1] = uv_offset_[1]; 410 | } 411 | 412 | void Preview::deactivate_pan() 413 | { 414 | pan_active_ = false; 415 | } 416 | -------------------------------------------------------------------------------- /src/Preview.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "ColorMaps.hpp" 6 | 7 | struct GLFWwindow; 8 | 9 | struct Rect2D 10 | { 11 | int x1; 12 | int y1; 13 | int x2; 14 | int y2; 15 | }; 16 | 17 | enum class Tonemap : int 18 | { 19 | ACES = 1, 20 | Reinhard = 2, 21 | Hable = 3, 22 | }; 23 | 24 | class Preview 25 | { 26 | public: 27 | enum class Quadrant 28 | { 29 | TopLeft = 0, 30 | TopRight = 1, 31 | BottomRight = 2, 32 | BottomLeft = 3, 33 | }; 34 | 35 | // Initialize pipelines, descriptor sets, etc. 36 | static void init(VkRenderPass render_pass); 37 | 38 | void reset_viewport(); 39 | void set_exposure(float exposure); 40 | void set_quadrant(Quadrant quadrant); 41 | void set_tonemap(Tonemap tonemap); 42 | void set_image(Image& image); 43 | void set_viewport(Rect2D viewport); 44 | void set_color_map(ColorMap color_map); 45 | void render(GLFWwindow* window, VkCommandBuffer cb); 46 | Image const* image() const 47 | { 48 | return image_; 49 | } 50 | 51 | // Magnify or minify the image, attemping to keep the position under the 52 | // cursor fixed 53 | void zoom(GLFWwindow* window, bool magnify); 54 | void activate_pan(GLFWwindow* window); 55 | void deactivate_pan(); 56 | 57 | private: 58 | void viewport_extent(int& vw, int& vh) const; 59 | float viewport_aspect() const; 60 | float max_scale() const; 61 | float min_scale() const; 62 | 63 | Image* image_ = nullptr; 64 | Rect2D viewport_ = {}; 65 | VkViewport preview_viewport_ = {}; 66 | VkRect2D preview_scissor_ = {}; 67 | float uv_offset_[2] = {0.f, 0.f}; 68 | float uv_scale_ = 1.f; 69 | double cursor_[2] = {0, 0}; 70 | float old_uv_offset_[2] = {0.f, 0.f}; 71 | float exposure_ = 1.f; 72 | Tonemap tonemap_ = Tonemap::ACES; 73 | uint32_t color_map_ = 0; 74 | bool use_color_map_ = false; 75 | bool pan_active_ = false; 76 | bool viewport_dirty_ = false; 77 | Quadrant quadrant_ = Quadrant::TopLeft; 78 | }; 79 | -------------------------------------------------------------------------------- /src/UI.cpp: -------------------------------------------------------------------------------- 1 | #include "UI.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace flop; 10 | 11 | static nfdfilteritem_t s_filter_list[] = {{"Images", "png,jpg,jpeg,bmp,exr"}}; 12 | static nfdfilteritem_t s_output_list[] = {{"PNG", "png"}}; 13 | 14 | 15 | extern void flop_init_reference(char const* reference_path); 16 | extern void flop_init_test(char const* test_path); 17 | extern int flop_analyze_impl(char const* reference_path, 18 | char const* test_path, 19 | char const* output_path, 20 | float exposure, 21 | int tonemapper, 22 | FlopSummary* out_summary, 23 | bool bypass_initialization); 24 | 25 | void UI::set_reference(std::string const& reference) 26 | { 27 | if (!reference.empty()) 28 | { 29 | reference_path_ = const_cast(reference.data()); 30 | } 31 | } 32 | 33 | void UI::set_test(std::string const& test) 34 | { 35 | if (!test.empty()) 36 | { 37 | test_path_ = const_cast(test.data()); 38 | } 39 | } 40 | 41 | void UI::set_output(std::string const& output) 42 | { 43 | if (!output.empty()) 44 | { 45 | output_path_ = const_cast(output.data()); 46 | } 47 | } 48 | 49 | void UI::on_scroll(GLFWwindow* window, bool magnify) 50 | { 51 | error_preview_.zoom(window, magnify); 52 | left_preview_.zoom(window, magnify); 53 | right_preview_.zoom(window, magnify); 54 | } 55 | 56 | void UI::on_click(GLFWwindow* window, int button, int action) 57 | { 58 | if (button == GLFW_MOUSE_BUTTON_LEFT) 59 | { 60 | if (action == GLFW_PRESS) 61 | { 62 | error_preview_.activate_pan(window); 63 | left_preview_.activate_pan(window); 64 | right_preview_.activate_pan(window); 65 | } 66 | else 67 | { 68 | error_preview_.deactivate_pan(); 69 | left_preview_.deactivate_pan(); 70 | right_preview_.deactivate_pan(); 71 | } 72 | } 73 | } 74 | 75 | void UI::set_tonemap(Tonemap tonemap) 76 | { 77 | tonemap_ = tonemap; 78 | } 79 | 80 | void UI::set_exposure(float exposure) 81 | { 82 | exposure_ = exposure; 83 | } 84 | 85 | bool UI::analyze(bool issued_from_gui) 86 | { 87 | bool disabled = !(reference_path_ && test_path_); 88 | if (disabled) 89 | { 90 | return false; 91 | } 92 | 93 | if (issued_from_gui) 94 | { 95 | if (flop_analyze_impl(reference_path_, 96 | test_path_, 97 | output_path_, 98 | exposure_, 99 | static_cast(tonemap_), 100 | &summary_, 101 | true)) 102 | { 103 | error_ = flop_get_error(); 104 | active_ = false; 105 | return false; 106 | } 107 | } 108 | else 109 | { 110 | if (flop_analyze_hdr(reference_path_, 111 | test_path_, 112 | output_path_, 113 | exposure_, 114 | static_cast(tonemap_), 115 | &summary_)) 116 | { 117 | error_ = flop_get_error(); 118 | active_ = false; 119 | return false; 120 | } 121 | } 122 | 123 | error_ = nullptr; 124 | error_preview_.set_image(g_error); 125 | error_preview_.set_quadrant(Preview::Quadrant::TopLeft); 126 | error_preview_.set_color_map(color_map_); 127 | viewport_dirty_ = true; 128 | active_ = true; 129 | error_max_ = 0.f; 130 | for (size_t i = 0; i != 32; ++i) 131 | { 132 | error_histogram_[i] = static_cast(g_error_histogram.data_)[i]; 133 | 134 | if (error_histogram_[i] > error_max_) 135 | { 136 | error_max_ = error_histogram_[i]; 137 | } 138 | } 139 | 140 | if (issued_from_gui) 141 | { 142 | ImGui::CloseCurrentPopup(); 143 | } 144 | return true; 145 | } 146 | 147 | void UI::update() 148 | { 149 | ImGui::NewFrame(); 150 | ImVec2 size = ImGui::GetMainViewport()->Size; 151 | ImGui::SetNextWindowPos(ImVec2{size.x * 0.5f, 0.f}); 152 | ImGui::SetNextWindowSize(ImVec2{size.x * 0.5f, size.y * 0.5f}); 153 | 154 | if (ImGui::Begin("Flop", 155 | nullptr, 156 | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar 157 | | ImGuiWindowFlags_NoResize)) 158 | { 159 | bool disabled = !(reference_path_ && test_path_); 160 | ImGui::BeginDisabled(disabled); 161 | 162 | if (!disabled) 163 | { 164 | ImGui::PushStyleColor(ImGuiCol_Button, ImVec4{0x00, 0xac, 0xc1, 1.f}); 165 | ImGui::PushStyleColor( 166 | ImGuiCol_ButtonHovered, ImVec4{0x5d, 0xde, 0xf4, 1.f}); 167 | ImGui::PushStyleColor(ImGuiCol_Text, ImVec4{0, 0, 0, 1.f}); 168 | } 169 | if (ImGui::Button("Start analysis")) 170 | { 171 | reset_viewports(); 172 | analyze(true); 173 | } 174 | if (!disabled) 175 | { 176 | ImGui::PopStyleColor(3); 177 | } 178 | 179 | ImGui::EndDisabled(); 180 | ImGui::SameLine(); 181 | if (ImGui::Button("Reset viewports")) 182 | { 183 | reset_viewports(); 184 | } 185 | 186 | if (error_) 187 | { 188 | ImGui::Text("%s", error_); 189 | } 190 | ImGui::Spacing(); 191 | ImGui::Separator(); 192 | ImGui::Spacing(); 193 | ImGui::Columns(3); 194 | ImGui::Text("%s", reference_path_ ? reference_path_ : "(None selected)"); 195 | if (ImGui::Button("Select reference image")) 196 | { 197 | NFD_OpenDialog(&reference_path_, s_filter_list, 1, nullptr); 198 | if (reference_path_) 199 | { 200 | flop_init_reference(reference_path_); 201 | toggled_ = false; 202 | left_preview_.set_image(g_reference.source_); 203 | left_preview_.set_quadrant(Preview::Quadrant::BottomLeft); 204 | reset_viewports(); 205 | viewport_dirty_ = true; 206 | active_ = false; 207 | 208 | if (g_reference.source_.width_ != g_test.source_.width_ 209 | || g_reference.source_.height_ != g_test.source_.height_) 210 | { 211 | g_test.source_.reset(); 212 | g_error.reset(); 213 | } 214 | } 215 | } 216 | ImGui::NextColumn(); 217 | 218 | ImGui::Text("%s", test_path_ ? test_path_ : "(None selected)"); 219 | if (ImGui::Button("Select test image")) 220 | { 221 | NFD_OpenDialog(&test_path_, s_filter_list, 1, nullptr); 222 | if (test_path_) 223 | { 224 | flop_init_test(test_path_); 225 | toggled_ = false; 226 | right_preview_.set_image(g_test.source_); 227 | right_preview_.set_quadrant(Preview::Quadrant::BottomRight); 228 | reset_viewports(); 229 | viewport_dirty_ = true; 230 | active_ = false; 231 | 232 | if (g_reference.source_.width_ != g_test.source_.width_ 233 | || g_reference.source_.height_ != g_test.source_.height_) 234 | { 235 | g_reference.source_.reset(); 236 | g_error.reset(); 237 | } 238 | } 239 | } 240 | 241 | ImGui::NextColumn(); 242 | ImGui::Text( 243 | "%s", output_path_ ? output_path_ : "(No save file selected)"); 244 | if (ImGui::Button("Select output location")) 245 | { 246 | NFD_SaveDialog(&output_path_, s_output_list, 1, nullptr, "flop.png"); 247 | } 248 | 249 | ImGui::Columns(1); 250 | 251 | ImGui::Spacing(); 252 | ImGui::Separator(); 253 | 254 | bool hdr = g_reference.source_.hdr_; 255 | ImGui::BeginDisabled(!hdr); 256 | 257 | if (ImGui::SliderFloat("Exposure", &exposure_, -15.f, 3.f)) 258 | { 259 | float exposure = std::powf(2.f, exposure_); 260 | left_preview_.set_exposure(exposure); 261 | right_preview_.set_exposure(exposure); 262 | } 263 | 264 | Tonemap previous_tonemap = tonemap_; 265 | ImGui::Text("Tonemapping operator"); 266 | if (ImGui::RadioButton("ACES", tonemap_ == Tonemap::ACES)) 267 | { 268 | tonemap_ = Tonemap::ACES; 269 | } 270 | ImGui::SameLine(); 271 | if (ImGui::RadioButton("Reinhard", tonemap_ == Tonemap::Reinhard)) 272 | { 273 | tonemap_ = Tonemap::Reinhard; 274 | } 275 | ImGui::SameLine(); 276 | if (ImGui::RadioButton("Hable", tonemap_ == Tonemap::Hable)) 277 | { 278 | tonemap_ = Tonemap::Hable; 279 | } 280 | 281 | if (tonemap_ != previous_tonemap) 282 | { 283 | left_preview_.set_tonemap(tonemap_); 284 | right_preview_.set_tonemap(tonemap_); 285 | } 286 | 287 | ImGui::EndDisabled(); 288 | 289 | ImGui::Checkbox("Toggle positions", &toggled_); 290 | ImGui::SameLine(); 291 | if (ImGui::Checkbox("Animate toggle", &animated_)) 292 | { 293 | if (animated_) 294 | { 295 | last_toggled_ = glfwGetTime(); 296 | } 297 | else 298 | { 299 | left_preview_.set_image(g_reference.source_); 300 | } 301 | } 302 | ImGui::SameLine(); 303 | ImGui::SliderFloat("Toggle interval (s)", &toggle_interval_s_, 0.1f, 3.f); 304 | 305 | ImGui::Spacing(); 306 | 307 | if (active_) 308 | { 309 | if (animated_) 310 | { 311 | if (left_preview_.image() == &g_reference.source_) 312 | { 313 | ImGui::Text("Viewing reference image."); 314 | } 315 | else 316 | { 317 | ImGui::Text("Viewing test image."); 318 | } 319 | } 320 | else if (toggled_) 321 | { 322 | ImGui::Text( 323 | "Viewing test image on the left, reference image on " 324 | "the right."); 325 | } 326 | else 327 | { 328 | ImGui::Text( 329 | "Viewing reference image on the left, test image on " 330 | "the right."); 331 | } 332 | 333 | ImGui::Spacing(); 334 | 335 | if (ImGui::RadioButton("Magma", color_map_ == ColorMap::Magma)) 336 | { 337 | error_preview_.set_color_map(ColorMap::Magma); 338 | color_map_ = ColorMap::Magma; 339 | } 340 | ImGui::SameLine(); 341 | if (ImGui::RadioButton("Viridis", color_map_ == ColorMap::Viridis)) 342 | { 343 | error_preview_.set_color_map(ColorMap::Viridis); 344 | color_map_ = ColorMap::Viridis; 345 | } 346 | ImGui::SameLine(); 347 | if (ImGui::RadioButton("Plasma", color_map_ == ColorMap::Plasma)) 348 | { 349 | error_preview_.set_color_map(ColorMap::Plasma); 350 | color_map_ = ColorMap::Plasma; 351 | } 352 | 353 | ImGui::Text("Reference/Test Image View Mode"); 354 | if (ImGui::RadioButton("Source", view_mode_ == ViewMode::Source)) 355 | { 356 | view_mode_ = ViewMode::Source; 357 | } 358 | ImGui::SameLine(); 359 | if (ImGui::RadioButton( 360 | "Filtered Source", view_mode_ == ViewMode::FilteredSource)) 361 | { 362 | view_mode_ = ViewMode::FilteredSource; 363 | } 364 | ImGui::SameLine(); 365 | if (ImGui::RadioButton("YyCxCz", view_mode_ == ViewMode::YyCxCz)) 366 | { 367 | view_mode_ = ViewMode::YyCxCz; 368 | } 369 | 370 | ImGui::Spacing(); 371 | 372 | ImGui::PlotHistogram("Error histogram", 373 | error_histogram_, 374 | 32, 375 | 0, 376 | nullptr, 377 | 0.f, 378 | error_max_, 379 | ImVec2(std::min(size.x / 3.4f, 300.f), 380 | std::min(size.y / 4, 140.f)), 381 | 4); 382 | } 383 | } 384 | ImGui::End(); 385 | } 386 | 387 | void UI::render(GLFWwindow* window, VkCommandBuffer cb) 388 | { 389 | ImGuiViewport& viewport = *ImGui::GetMainViewport(); 390 | if (reference_path_ && !error_) 391 | { 392 | if (viewport_dirty_) 393 | { 394 | Rect2D preview_viewport; 395 | preview_viewport.x1 = 0; 396 | preview_viewport.y1 = viewport.WorkPos.y; 397 | preview_viewport.x2 = viewport.WorkSize.x; 398 | preview_viewport.y2 = viewport.WorkSize.y + preview_viewport.y1; 399 | left_preview_.set_viewport(preview_viewport); 400 | right_preview_.set_viewport(preview_viewport); 401 | error_preview_.set_viewport(preview_viewport); 402 | viewport_dirty_ = false; 403 | } 404 | 405 | if (animated_) 406 | { 407 | double now = glfwGetTime(); 408 | if (now - last_toggled_ > toggle_interval_s_) 409 | { 410 | last_toggled_ = now; 411 | toggled_ = !toggled_; 412 | } 413 | } 414 | else 415 | { 416 | right_preview_.render(window, cb); 417 | } 418 | ImagePacket& left = toggled_ ? g_test : g_reference; 419 | ImagePacket& right = toggled_ ? g_reference : g_test; 420 | 421 | switch (view_mode_) 422 | { 423 | using enum ViewMode; 424 | case Source: 425 | left_preview_.set_image(left.source_); 426 | right_preview_.set_image(right.source_); 427 | break; 428 | case YyCxCz: 429 | left_preview_.set_image(left.yycxcz_); 430 | right_preview_.set_image(right.yycxcz_); 431 | break; 432 | case FilteredSource: 433 | left_preview_.set_image(left.yycxcz_blurred_); 434 | right_preview_.set_image(right.yycxcz_blurred_); 435 | break; 436 | default: 437 | left_preview_.set_image(left.source_); 438 | right_preview_.set_image(right.source_); 439 | break; 440 | } 441 | left_preview_.render(window, cb); 442 | error_preview_.render(window, cb); 443 | } 444 | 445 | ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cb); 446 | } 447 | 448 | void UI::mark_viewport_dirty() 449 | { 450 | viewport_dirty_ = true; 451 | } 452 | 453 | void UI::reset_viewports() 454 | { 455 | left_preview_.reset_viewport(); 456 | right_preview_.reset_viewport(); 457 | } 458 | -------------------------------------------------------------------------------- /src/UI.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ColorMaps.hpp" 4 | #include "Preview.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class UI 12 | { 13 | public: 14 | enum class ViewMode 15 | { 16 | Source, 17 | FilteredSource, 18 | YyCxCz, 19 | Edge, 20 | EdgeFiltered, 21 | }; 22 | 23 | void on_scroll(GLFWwindow* window, bool magnify); 24 | 25 | void on_click(GLFWwindow* window, int button, int action); 26 | 27 | void update(); 28 | 29 | void render(GLFWwindow* window, VkCommandBuffer cb); 30 | 31 | void mark_viewport_dirty(); 32 | 33 | void set_reference(std::string const& reference); 34 | 35 | void set_test(std::string const& test); 36 | 37 | void set_tonemap(Tonemap tonemap); 38 | 39 | void set_exposure(float exposure); 40 | 41 | void set_output(std::string const& output); 42 | 43 | bool analyze(bool issued_from_ui); 44 | 45 | private: 46 | void reset_viewports(); 47 | 48 | FlopSummary summary_; 49 | Preview left_preview_; 50 | Preview right_preview_; 51 | Preview error_preview_; 52 | char const* error_ = nullptr; 53 | nfdchar_t* reference_path_ = nullptr; 54 | nfdchar_t* test_path_ = nullptr; 55 | nfdchar_t* output_path_ = nullptr; 56 | double last_toggled_ = 0.0; 57 | float toggle_interval_s_ = 0.75f; 58 | float error_histogram_[32] = {}; 59 | float error_max_ = 0.f; 60 | float exposure_ = 0.f; 61 | Tonemap tonemap_ = Tonemap::ACES; 62 | ColorMap color_map_ = ColorMap::Magma; 63 | bool active_ = false; 64 | bool viewport_dirty_ = false; 65 | bool toggled_ = false; 66 | bool animated_ = false; 67 | ViewMode view_mode_ = ViewMode::Source; 68 | }; 69 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable( 2 | flop_tests 3 | Test.cpp 4 | ) 5 | 6 | target_compile_features( 7 | flop_tests 8 | PUBLIC 9 | cxx_std_20 10 | ) 11 | 12 | target_link_libraries( 13 | flop_tests 14 | PUBLIC 15 | lflop 16 | ) 17 | -------------------------------------------------------------------------------- /test/Test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | int main(int argc, char const* argv[]) 7 | { 8 | std::filesystem::path base{__FILE__}; 9 | base = base.parent_path(); 10 | 11 | std::string reference_path = (base / "reference.png").string(); 12 | std::string test_path = (base / "test.png").string(); 13 | std::string output_path = (base / "flop_ldr.png").string(); 14 | 15 | flop_analyze( 16 | reference_path.c_str(), test_path.c_str(), output_path.c_str(), nullptr); 17 | 18 | reference_path = (base / "reference.exr").string(); 19 | test_path = (base / "test.exr").string(); 20 | output_path = (base / "flop_hdr.png").string(); 21 | 22 | flop_analyze_hdr(reference_path.c_str(), 23 | test_path.c_str(), 24 | output_path.c_str(), 25 | -2.f, 26 | 0, 27 | nullptr); 28 | 29 | return 0; 30 | } 31 | -------------------------------------------------------------------------------- /test/flop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/flop.png -------------------------------------------------------------------------------- /test/flop_hdr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/flop_hdr.png -------------------------------------------------------------------------------- /test/flop_ldr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/flop_ldr.png -------------------------------------------------------------------------------- /test/reference.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/reference.exr -------------------------------------------------------------------------------- /test/reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/reference.png -------------------------------------------------------------------------------- /test/reference2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/reference2.png -------------------------------------------------------------------------------- /test/reference3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/reference3.png -------------------------------------------------------------------------------- /test/test.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/test.exr -------------------------------------------------------------------------------- /test/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/test.png -------------------------------------------------------------------------------- /test/test2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyong/flop/09199d49672fa06df5b23d1988a129da5b0a782e/test/test2.png --------------------------------------------------------------------------------