From dee767a487357ef684165849213b421e4786ed69 Mon Sep 17 00:00:00 2001 From: Gwendolyn Date: Sun, 21 May 2023 11:35:46 +0200 Subject: [PATCH] working but missing a bunch of features and slightly buggy --- .clang-format | 1 + .envrc | 1 + CMakeLists.txt | 132 ++- EmbedResources.cmake | 336 ++++++ main | Bin 15416 -> 0 bytes main.cpp | 182 --- preprocess-shader.sh | 26 + readme.md | 87 ++ shaders/automaton.comp | 104 ++ shaders/automaton.frag | 108 ++ shaders/automaton.vert | 9 + shaders/cellstate_common.glsl | 22 + shaders/color_conversion.glsl | 80 ++ shell.nix | 7 + src/argparser.h | 1679 ++++++++++++++++++++++++++ src/color.cpp | 321 +++++ src/color.h | 108 ++ src/colormaps/colormaps.cpp | 37 + src/colormaps/colormaps.h | 58 + src/colormaps/dualcolor.cpp | 33 + src/colormaps/rainbow.cpp | 12 + src/colormaps/viridis.cpp | 2081 +++++++++++++++++++++++++++++++++ src/distinct-types.h | 52 + src/main.cpp | 16 + src/options.cpp | 184 +++ src/options.h | 300 +++++ src/program.cpp | 1099 +++++++++++++++++ src/program.h | 118 ++ src/rule_presets.cpp | 25 + src/rule_presets.h | 11 + src/shader-helper.cpp | 51 + src/shader-helper.h | 13 + tests/color.cpp | 229 ++++ 33 files changed, 7337 insertions(+), 185 deletions(-) create mode 100644 .clang-format create mode 100644 .envrc create mode 100644 EmbedResources.cmake delete mode 100755 main delete mode 100644 main.cpp create mode 100755 preprocess-shader.sh create mode 100644 readme.md create mode 100644 shaders/automaton.comp create mode 100644 shaders/automaton.frag create mode 100644 shaders/automaton.vert create mode 100644 shaders/cellstate_common.glsl create mode 100644 shaders/color_conversion.glsl create mode 100644 shell.nix create mode 100644 src/argparser.h create mode 100644 src/color.cpp create mode 100644 src/color.h create mode 100644 src/colormaps/colormaps.cpp create mode 100644 src/colormaps/colormaps.h create mode 100644 src/colormaps/dualcolor.cpp create mode 100644 src/colormaps/rainbow.cpp create mode 100644 src/colormaps/viridis.cpp create mode 100644 src/distinct-types.h create mode 100644 src/main.cpp create mode 100644 src/options.cpp create mode 100644 src/options.h create mode 100644 src/program.cpp create mode 100644 src/program.h create mode 100644 src/rule_presets.cpp create mode 100644 src/rule_presets.h create mode 100644 src/shader-helper.cpp create mode 100644 src/shader-helper.h create mode 100644 tests/color.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..594a340 --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +ColumnLimit: 0 diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..051d09d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +eval "$(lorri direnv)" diff --git a/CMakeLists.txt b/CMakeLists.txt index c49f8da..23243cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,137 @@ -cmake_minimum_required(VERSION 3.24) +cmake_minimum_required(VERSION 3.25) project(shader-automaton) +set(CMAKE_CXX_STANDARD 23) +set(VERBOSE yes) find_package(OpenGL REQUIRED) find_package(GLEW REQUIRED) find_package(glfw3 3.3 REQUIRED) +include(FetchContent) + +include(EmbedResources.cmake) + +FetchContent_Declare( + perlin + GIT_REPOSITORY https://github.com/Reputeless/PerlinNoise.git + GIT_TAG bdf39fe92b2a585cdef485bcec2bca8ab5614095 +) + +FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG 70cca1eac07aa3809bdd3717253c3754a5b7cfcc +) + +FetchContent_Declare( + gtest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG bc860af08783b8113005ca7697da5f5d49a8056f +) + +FetchContent_Declare( + dejavufonts + URL https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.tar.bz2 + URL_HASH SHA1=7fa15e7b9676fc3915338c41e76ad454c344fff5 +) + +FetchContent_MakeAvailable(perlin) +FetchContent_MakeAvailable(imgui) -add_executable(shader-automaton main.cpp) -target_link_libraries(shader-automaton PRIVATE OpenGL::GL GLEW::GLEW glfw) \ No newline at end of file +add_executable(shader-automaton + src/main.cpp + src/options.cpp + src/colormaps/viridis.cpp + src/rule_presets.cpp + src/argparser.h + + src/rule_presets.h + src/options.h + src/shader-helper.cpp + src/shader-helper.h + src/color.cpp + src/color.h + src/colormaps/colormaps.cpp + src/colormaps/dualcolor.cpp + src/colormaps/viridis.cpp + src/colormaps/rainbow.cpp + src/program.cpp + src/program.h + ) + +target_compile_definitions(shader-automaton PRIVATE PROGRAM_NAME="${CMAKE_PROJECT_NAME}" PROGRAM_VERSION="${CMAKE_PROJECT_VERSION}") + +function(preprocess_shader name) + add_custom_command( + OUTPUT "preprocessed-shaders/${name}" + COMMAND ./preprocess-shader.sh "${name}" + DEPENDS "shaders/${name}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + PRE_BUILD + VERBATIM + ) +endfunction() + + +preprocess_shader("automaton.comp") +preprocess_shader("automaton.vert") +preprocess_shader("automaton.frag") + +# TODO: this whole shader preprocessing and embedding is just broken +# it happens half at configure time rather than build time so the preprocessed files are outdated + +# TODO: maybe rewrite this tool to put multiple namespaces into one target +# and to maybe define the contents separately from the target, if possible +add_embedded_binary_resources( + shader-automaton-shaders + OUT_DIR embedded-resources + HEADER shaders.h + NAMESPACE resources::shaders + RESOURCE_NAMES automaton_comp automaton_vert automaton_frag + RESOURCES + "${PROJECT_SOURCE_DIR}/preprocessed-shaders/automaton.comp" + "${PROJECT_SOURCE_DIR}/preprocessed-shaders/automaton.vert" + "${PROJECT_SOURCE_DIR}/preprocessed-shaders/automaton.frag" +) + + +FetchContent_MakeAvailable(dejavufonts) +add_embedded_binary_resources( + shader-automaton-fonts + OUT_DIR embedded-resources + HEADER fonts.h + NAMESPACE resources::fonts + RESOURCE_NAMES DejaVuSansMono + RESOURCES + "${dejavufonts_SOURCE_DIR}/ttf/DejaVuSansMono.ttf" +) + + + +target_link_libraries(shader-automaton OpenGL::GL GLEW::GLEW glfw shader-automaton-shaders shader-automaton-fonts) + +file(COPY_FILE "${perlin_SOURCE_DIR}/PerlinNoise.hpp" "${perlin_BINARY_DIR}/PerlinNoise.hpp") +target_include_directories(shader-automaton PRIVATE ${perlin_BINARY_DIR}) + +target_include_directories(shader-automaton PRIVATE "${imgui_SOURCE_DIR}/backends" "${imgui_SOURCE_DIR}") +target_sources(shader-automaton PRIVATE + ${imgui_SOURCE_DIR}/imgui.cpp + ${imgui_SOURCE_DIR}/imgui_demo.cpp + ${imgui_SOURCE_DIR}/imgui_draw.cpp + ${imgui_SOURCE_DIR}/imgui_tables.cpp + ${imgui_SOURCE_DIR}/imgui_widgets.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp + ) + + +enable_testing() +FetchContent_MakeAvailable(gtest) + +include(GoogleTest) + +add_executable(ColorTest tests/color.cpp src/color.cpp) +target_link_libraries(ColorTest GTest::gtest_main) +target_include_directories(ColorTest PRIVATE src) +gtest_discover_tests(ColorTest) \ No newline at end of file diff --git a/EmbedResources.cmake b/EmbedResources.cmake new file mode 100644 index 0000000..1f75300 --- /dev/null +++ b/EmbedResources.cmake @@ -0,0 +1,336 @@ +# Module for embedding resources in binaries + +find_program(HEXDUMP_COMMAND NAMES xxd) + +#[==[ + Adds an object library target containing embedded binary resources and + generates a header file, where you can access them as `span`. + + To use with older C++ standards, you can override both the `span` template + and the `byte` type to use with e.g. span-lite, Microsoft.GSL or range-v3 + (don't forget to link your span library of choice to the generated target). + + Usage: + add_embedded_binary_resources( + name # Name of the created target + + OUT_DIR # Directory where the header and sources will be created + # (relative to the build directory) + HEADER
# Name of the generated header + NAMESPACE [namespace] # Namespace of created symbols (optional) + RESOURCE_NAMES # Names (symbols) of the resources + RESOURCES # Resource files + SPAN_TEMPLATE [template name] # Name of the `span` template, default `std::span` + SPAN_HEADER [header] # Header with the `span` template, default `` + BYTE_TYPE [type name] # Name of the `byte` type, default `std::byte` + BYTE_HEADER [header] # Header with the `byte` type, default `` + ) +]==] +function(add_embedded_binary_resources NAME) + set(OPTIONS "") + set(ONE_VALUE_ARGS OUT_DIR HEADER NAMESPACE SPAN_TEMPLATE SPAN_HEADER BYTE_TYPE BYTE_HEADER) + set(MULTI_VALUE_ARGS RESOURCE_NAMES RESOURCES) + cmake_parse_arguments(ARGS "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) + + if(NOT HEXDUMP_COMMAND) + message(FATAL_ERROR "Cannot embed resources - xxd not found.") + endif() + + set(FULL_HEADER_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${ARGS_HEADER}") + + if(NOT DEFINED ARGS_SPAN_TEMPLATE) + set(ARGS_SPAN_TEMPLATE "std::span") + endif() + + if(NOT DEFINED ARGS_SPAN_HEADER) + set(ARGS_SPAN_HEADER "") + endif() + + if(NOT DEFINED ARGS_BYTE_TYPE) + set(ARGS_BYTE_TYPE "std::byte") + endif() + + if(NOT DEFINED ARGS_BYTE_HEADER) + set(ARGS_BYTE_HEADER "") + endif() + + add_library("${NAME}" OBJECT) + target_include_directories("${NAME}" PUBLIC "${CMAKE_CURRENT_BINARY_DIR}") + + if(ARGS_SPAN_HEADER STREQUAL "") + target_compile_features("${NAME}" PUBLIC cxx_std_20) + elseif(ARGS_BYTE_HEADER STREQUAL "") + target_compile_features("${NAME}" PUBLIC cxx_std_17) + endif() + + # fPIC not added automatically to object libraries due to defect in CMake + set_target_properties( + "${NAME}" + + PROPERTIES + POSITION_INDEPENDENT_CODE ON + ) + + file( + WRITE "${FULL_HEADER_PATH}" + + "#pragma once\n" + "\n" + "#include ${ARGS_BYTE_HEADER}\n" + "#include ${ARGS_SPAN_HEADER}\n" + "\n" + ) + + if(DEFINED ARGS_NAMESPACE) + file( + APPEND "${FULL_HEADER_PATH}" + + "namespace ${ARGS_NAMESPACE}\n" + "{\n" + "\n" + ) + endif() + + foreach(RESOURCE_NAME RESOURCE IN ZIP_LISTS ARGS_RESOURCE_NAMES ARGS_RESOURCES) + set(FULL_RESOURCE_UNIT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${RESOURCE_NAME}.cpp") + set(FULL_RESOURCE_HEX_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${RESOURCE_NAME}.inc") + + # Add symbol to header + file( + APPEND "${FULL_HEADER_PATH}" + + "[[nodiscard]] ${ARGS_SPAN_TEMPLATE}<${ARGS_BYTE_TYPE} const>\n" + "${RESOURCE_NAME}() noexcept;\n" + "\n" + ) + + # Write .cpp + file( + WRITE "${FULL_RESOURCE_UNIT_PATH}" + + "#include \"${ARGS_HEADER}\"\n" + "\n" + "#include \n" + "\n" + ) + + if(DEFINED ARGS_NAMESPACE) + file( + APPEND "${FULL_RESOURCE_UNIT_PATH}" + + "namespace ${ARGS_NAMESPACE}\n" + "{\n" + "\n" + ) + endif() + + file( + APPEND "${FULL_RESOURCE_UNIT_PATH}" + + "namespace\n" + "{\n" + "\n" + "std::uint8_t const ${RESOURCE_NAME}_data[] = {\n" + "#include \"${RESOURCE_NAME}.inc\"\n" + "};\n" + "\n" + "} // namespace\n" + "\n" + "${ARGS_SPAN_TEMPLATE}<${ARGS_BYTE_TYPE} const>\n" + "${RESOURCE_NAME}() noexcept\n" + "{\n" + " return as_bytes(${ARGS_SPAN_TEMPLATE}{${RESOURCE_NAME}_data, sizeof(${RESOURCE_NAME}_data)});\n" + "}\n" + ) + + if(DEFINED ARGS_NAMESPACE) + file( + APPEND "${FULL_RESOURCE_UNIT_PATH}" + + "\n" + "} // namespace ${ARGS_NAMESPACE}\n" + ) + endif() + + target_sources("${NAME}" PRIVATE "${FULL_RESOURCE_UNIT_PATH}") + + add_custom_command( + OUTPUT "${FULL_RESOURCE_HEX_PATH}" + COMMAND "${HEXDUMP_COMMAND}" -i < "${RESOURCE}" > "${FULL_RESOURCE_HEX_PATH}" + MAIN_DEPENDENCY "${RESOURCE}" + ) + list(APPEND RESOURCES_HEX_FILES "${FULL_RESOURCE_HEX_PATH}") + endforeach() + + if(DEFINED ARGS_NAMESPACE) + file( + APPEND "${FULL_HEADER_PATH}" + + "} // namespace ${ARGS_NAMESPACE}\n" + ) + endif() + + target_sources("${NAME}" PUBLIC "${FULL_HEADER_PATH}") + + add_custom_target("${NAME}_hexdump" DEPENDS "${RESOURCES_HEX_FILES}") + add_dependencies("${NAME}" "${NAME}_hexdump") +endfunction() + +#[==[ + Adds an object library target containing embedded text resources and + generates a header file, where you can access them as `string_view`. + + To use with older C++ standards, you can override the `string_view` type + to use with e.g. string-view-lite (don't forget to link your string view + library of choice to the generated target). + + Usage: + add_embedded_text_resources( + name # Name of the created target + + OUT_DIR # Directory where the header and sources will be created + # (relative to the build directory) + HEADER
# Name of the generated header + NAMESPACE [namespace] # Namespace of created symbols (optional) + RESOURCE_NAMES # Names (symbols) of the resources + RESOURCES # Resource files + STRING_VIEW_TYPE [type name] # Name of the `string_view` type, default `std::string_view` + STRING_VIEW_HEADER [header] # Header with the `string_view` type, default `` + ) +]==] +function(add_embedded_text_resources NAME) + set(OPTIONS "") + set(ONE_VALUE_ARGS OUT_DIR HEADER NAMESPACE STRING_VIEW_TYPE STRING_VIEW_HEADER) + set(MULTI_VALUE_ARGS RESOURCE_NAMES RESOURCES) + cmake_parse_arguments(ARGS "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) + + if(NOT HEXDUMP_COMMAND) + message(FATAL_ERROR "Cannot embed resources - xxd not found.") + endif() + + set(FULL_HEADER_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${ARGS_HEADER}") + + if(NOT DEFINED ARGS_STRING_VIEW_TYPE) + set(ARGS_STRING_VIEW_TYPE "std::string_view") + endif() + + if(NOT DEFINED ARGS_STRING_VIEW_HEADER) + set(ARGS_STRING_VIEW_HEADER "") + endif() + + add_library("${NAME}" OBJECT) + target_include_directories("${NAME}" PUBLIC "${CMAKE_CURRENT_BINARY_DIR}") + + if(ARGS_STRING_VIEW_HEADER STREQUAL "") + target_compile_features("${NAME}" PUBLIC cxx_std_17) + endif() + + # fPIC not added automatically to object libraries due to defect in CMake + set_target_properties( + "${NAME}" + + PROPERTIES + POSITION_INDEPENDENT_CODE ON + ) + + file( + WRITE "${FULL_HEADER_PATH}" + + "#pragma once\n" + "\n" + "#include ${ARGS_STRING_VIEW_HEADER}\n" + "\n" + ) + + if(DEFINED ARGS_NAMESPACE) + file( + APPEND "${FULL_HEADER_PATH}" + + "namespace ${ARGS_NAMESPACE}\n" + "{\n" + "\n" + ) + endif() + + foreach(RESOURCE_NAME RESOURCE IN ZIP_LISTS ARGS_RESOURCE_NAMES ARGS_RESOURCES) + set(FULL_RESOURCE_UNIT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${RESOURCE_NAME}.cpp") + set(FULL_RESOURCE_HEX_PATH "${CMAKE_CURRENT_BINARY_DIR}/${ARGS_OUT_DIR}/${RESOURCE_NAME}.inc") + + # Add symbol to header + file( + APPEND "${FULL_HEADER_PATH}" + + "[[nodiscard]] ${ARGS_STRING_VIEW_TYPE}\n" + "${RESOURCE_NAME}() noexcept;\n" + "\n" + ) + + # Write .cpp + file( + WRITE "${FULL_RESOURCE_UNIT_PATH}" + + "#include \"${ARGS_HEADER}\"\n" + "\n" + ) + + if(DEFINED ARGS_NAMESPACE) + file( + APPEND "${FULL_RESOURCE_UNIT_PATH}" + + "namespace ${ARGS_NAMESPACE}\n" + "{\n" + "\n" + ) + endif() + + file( + APPEND "${FULL_RESOURCE_UNIT_PATH}" + + "namespace\n" + "{\n" + "\n" + "char const ${RESOURCE_NAME}_data[] = {\n" + "#include \"${RESOURCE_NAME}.inc\"\n" + "};\n" + "\n" + "} // namespace\n" + "\n" + "${ARGS_STRING_VIEW_TYPE}\n" + "${RESOURCE_NAME}() noexcept\n" + "{\n" + " return ${ARGS_STRING_VIEW_TYPE}{${RESOURCE_NAME}_data, sizeof(${RESOURCE_NAME}_data)};\n" + "}\n" + ) + + if(DEFINED ARGS_NAMESPACE) + file( + APPEND "${FULL_RESOURCE_UNIT_PATH}" + + "\n" + "} // namespace ${ARGS_NAMESPACE}\n" + ) + endif() + + target_sources("${NAME}" PRIVATE "${FULL_RESOURCE_UNIT_PATH}") + + add_custom_command( + OUTPUT "${FULL_RESOURCE_HEX_PATH}" + COMMAND "${HEXDUMP_COMMAND}" -i < "${RESOURCE}" > "${FULL_RESOURCE_HEX_PATH}" + MAIN_DEPENDENCY "${RESOURCE}" + ) + list(APPEND RESOURCES_HEX_FILES "${FULL_RESOURCE_HEX_PATH}") + endforeach() + + if(DEFINED ARGS_NAMESPACE) + file( + APPEND "${FULL_HEADER_PATH}" + + "} // namespace ${ARGS_NAMESPACE}\n" + ) + endif() + + target_sources("${NAME}" PUBLIC "${FULL_HEADER_PATH}") + + add_custom_target("${NAME}_hexdump" DEPENDS "${RESOURCES_HEX_FILES}") + add_dependencies("${NAME}" "${NAME}_hexdump") +endfunction() diff --git a/main b/main deleted file mode 100755 index d5826754c326784229ed4efd11c991f32deaff6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15416 zcmeHOU2Ggz6~4Pp8i&^FO{u|2+GYd+B@|EWwPRdTll5=L;QZh?qDT$Hcz10t+8=j! zOg9BlOKG5?iUdLego>0PA@YJ0i3mmTvXN0B6%Q@&Q1OCQQppv6sVJxgDZ@E;&NmrP zHx=*z2+gr}zk9xWzI*SSnc123oPBCydNP?vC|H~Nh+;3B_L;17Tp5rCK~@c^4tjr3 zJ)rI)f0O3?{;Utw>)T7ojo2=H8zJkuxzPge_h|*h3K>~|9TQ#J(8H)G?)GT~#C#too$OkPczeKZ zo{{!e#`2@;gp!SuDbAto>O8K5rv9jFgX$%ea3=Wv}s+rLf^SXNJ#mCg4BPUdH zMKzg&KA{pD{G|94qK#wkqZg;0^&?N;`0|%tKJcNf_kZ`B)9W+GAKRJw702*)@R%IK z#vH$3+=myqe+Exd(0f`0evG(#Tkw;2;CX#Ui+EmBEB=Fb;FWDJl&clH?m9Kswv|0K zd)UqwYK4Vj-7VB+5090qmBOquS1R~@Eqj)h-8zj#`z|LVm7QXRm|VlL=Zh7mRD80a zsJrgwa}PblyK|7;a?o1Hk&?YW=5s!OXnJaN%+8qo0}9QIxu3QqN&AoXto|dHEJ6O) z2yy&f{_5B1ux(S{7H)Y*aVzUCaxfV_7jQoIZu9wza^8bqlL6CNzaKpO=0Tt5bA)pb z`&o#WLtS^h5aPqf(@P;UA=ZW^-_j7vh+?Ayk`-|N7 z>YC?ytnd57)Y%t5Wu1N5I(_5jvDt~fX5V$|-0-g{#_P&a`0Mj#=dP#qc*I{I#yYq8 zTMQ4s;dar~lia4Mw|Z-xyH+@GRgmHz*YW#5!?5?3b@rxp^^XUwt8cVfiEGx2x7_<^ zfG!!J-CLXQ+%k3ipIogP`W zIJtUzy{q%L+r0fe-m41~dxFNGt!Sorw4biuxaE0Qc;P2kb&SqwkVWo@EzUrkfj9$k z2I36F8Hh6wXCTf%oPjt4aR%ZH{P!}z&#M*-rBc;+qFO8ERbpq`f!(wmewLHI>3NS5 z_7Y|Z_rB(ND}-$P?2;|<+HDF#Fh;WL~3flxcRCjIa5X?0ofZozKB zENRg8>wB$ufB$a_>cF9UfEo$arK0QR|0yJBuVVir{t;@VRk!NS zg7NW(1P1Z#5q^w#nw2}X$>FJH$ZJ&>T_*lsl~NDOd4ltxM*Oy9w?f|ZVD$DB$0s`# z&MWX|n1>U>A89@n{O&^k?}WJTFrTEeHnN{C63_ESx0lZ&&O>y2uWEj~GP2YN*)?jX z)%LfH^9$lzt@F3U8}y1A?{FhZlAlVn<`oxao4KVWD$b^(%$~69$(fPE6Sg%zqwMid z9vL}2HKy!C(?>@~rtPDXlgB4!?b(sh=?SV0FXSE9p)&BJROFp6R_x_^As;F7&V~!M zr3gG)goI;QL31CwKF@JRXzajcjiNpN~5P4!f-@`=p7l`AWVEo|v zAlC`pw^4|;=>C6>*s$IZU4Wtk;PM0fUPPaj4&XZ@I}AFE_x*?-*GVAmOA#Mk{|`iu z{3}Bu1*7vt^*@g2)6fa-rv9)+_1CGBHxcs>8GbbI^S&FrWB-BpB{ldMmD4u(9T0w} z7LA7w{0(V4LwekgU+f{03~|19+Xw$U>fmDs^W#1XzdOY53;FnGgARFpojBfp=<)lb z=01*AYL5@WJM_ResVA(*{oK0fjX>!)paUjJ9M0R TrF-Aj`t$w1;9U_x#8v+QvQJ;z diff --git a/main.cpp b/main.cpp deleted file mode 100644 index 979a036..0000000 --- a/main.cpp +++ /dev/null @@ -1,182 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -GLuint loadShader(const char *source, GLenum shader_type) { - GLuint shaderId = glCreateShader(shader_type); - GLint result = GL_FALSE; - int infoLogLength; - // Compile Vertex Shader - glShaderSource(shaderId, 1, &source, nullptr); - glCompileShader(shaderId); - glGetShaderiv(shaderId, GL_COMPILE_STATUS, &result); - glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &infoLogLength); - if (infoLogLength > 0) { - std::vector VertexShaderErrorMessage(infoLogLength + 1); - glGetShaderInfoLog(shaderId, infoLogLength, nullptr, - &VertexShaderErrorMessage[0]); - printf("%s\n", &VertexShaderErrorMessage[0]); - exit(-1); - } - return shaderId; -} - -GLuint loadProgram(const std::vector &shaders) { - GLuint programId = glCreateProgram(); - for (const auto &item : shaders) { - glAttachShader(programId, item); - } - glLinkProgram(programId); - - GLint result = GL_FALSE; - int infoLogLength; - - glGetProgramiv(programId, GL_LINK_STATUS, &result); - glGetProgramiv(programId, GL_INFO_LOG_LENGTH, &infoLogLength); - if (infoLogLength > 0) { - std::vector ProgramErrorMessage(infoLogLength + 1); - glGetProgramInfoLog(programId, infoLogLength, nullptr, - &ProgramErrorMessage[0]); - printf("%s\n", &ProgramErrorMessage[0]); - } - return programId; -} - -static const char *vertex_shader = - "#version 460\n" - "layout(location = 0) in vec3 vertexPosition_modelspace;\n" - "out vec2 coord;\n" - "void main(){\n" - " gl_Position.xyz = vertexPosition_modelspace;\n" - " gl_Position.w = 1.0;\n" - " coord.xy = vertexPosition_modelspace.xy;\n" - "}"; - -static const char *fragment_shader = - "#version 460\n" - "out vec3 color;\n" - "in vec2 coord;\n" - "layout (std430, binding = 2) restrict readonly buffer shader_data\n" - "{\n" - " int c[];\n" - "};\n" - - "void main(){\n" - " int x = int(255. * (coord.x + 1.) / 2.);" - " color = vec3(c[x]/255., 0.5,0.5);\n" - "}"; - -static const char *compute_shader = - "#version 460\n" - "layout(local_size_x = 1, local_size_y = 1) in;\n" - "layout (std430, binding = 2) restrict buffer shader_data\n" - "{\n" - " int c[];\n" - "};\n" - "void main(){\n" - " c[gl_GlobalInvocationID.x] = (c[gl_GlobalInvocationID.x] + 1) % 256;\n" - "}\n"; - -int main() { - glewExperimental = true; - if (!glfwInit()) { - fprintf(stderr, "Failed to initialize GLFW\n"); - return -1; - } - - glfwWindowHint(GLFW_SAMPLES, 0); - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); - glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - - GLFWwindow *window = - glfwCreateWindow(1024, 768, "Tutorial 01", nullptr, nullptr); - if (window == nullptr) { - fprintf(stderr, - "Failed to open GLFW window. If you have an Intel GPU, they are " - "not 3.3 compatible. Try the 2.1 version of the tutorials.\n"); - glfwTerminate(); - return -1; - } - glfwMakeContextCurrent(window); // Initialize GLEW - glewExperimental = true; // Needed in core profile - if (glewInit() != GLEW_OK) { - fprintf(stderr, "Failed to initialize GLEW\n"); - return -1; - } - - glfwSetInputMode(window, GLFW_STICKY_KEYS, GL_TRUE); - - GLuint VertexArrayID; - glGenVertexArrays(1, &VertexArrayID); - glBindVertexArray(VertexArrayID); - const GLfloat g_vertex_buffer_data[] = { - -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, - }; - GLuint vertexbuffer; - glGenBuffers(1, &vertexbuffer); - glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); - glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), - g_vertex_buffer_data, GL_STATIC_DRAW); - - GLuint programId = loadProgram({ - loadShader(vertex_shader, GL_VERTEX_SHADER), - loadShader(fragment_shader, GL_FRAGMENT_SHADER), - }); - - GLuint computeProgramId = loadProgram({ - loadShader(compute_shader, GL_COMPUTE_SHADER), - }); - - int c[256]; - - for (size_t i = 0; i < 256; ++i) { - c[i] = i; - } - - GLuint ssbo = 0; - glGenBuffers(1, &ssbo); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int) * 256, &c, - GL_DYNAMIC_COPY); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); - - unsigned int block_index = glGetProgramResourceIndex( - programId, GL_SHADER_STORAGE_BLOCK, "shader_data"); - GLuint ssbo_binding_point_index = 2; - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ssbo_binding_point_index, ssbo); - glShaderStorageBlockBinding(programId, block_index, ssbo_binding_point_index); - - glUseProgram(computeProgramId); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, ssbo_binding_point_index, ssbo); - unsigned int compute_block_index = glGetProgramResourceIndex( - programId, GL_SHADER_STORAGE_BLOCK, "shader_data"); - glShaderStorageBlockBinding(computeProgramId, compute_block_index, - ssbo_binding_point_index); - - do { - glClear(GL_COLOR_BUFFER_BIT); - - glUseProgram(computeProgramId); - glDispatchCompute(255, 1, 1); - - glUseProgram(programId); - - glEnableVertexAttribArray(0); - glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr); - glDrawArrays(GL_TRIANGLES, 0, 3); - glDisableVertexAttribArray(0); - - glfwSwapBuffers(window); - glfwPollEvents(); - - } // Check if the ESC key was pressed or the window was closed - while (glfwGetKey(window, GLFW_KEY_ESCAPE) != GLFW_PRESS && - glfwWindowShouldClose(window) == 0); -} \ No newline at end of file diff --git a/preprocess-shader.sh b/preprocess-shader.sh new file mode 100755 index 0000000..ac081d2 --- /dev/null +++ b/preprocess-shader.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +if [ "$#" -ne 1 ]; then + echo "exactly one argument required" + exit 1 +fi + +mkdir -p preprocessed-shaders + +preprocess_file() { + input_file="shaders/$1" + while IFS= read -r line || [ -n "$line" ]; do + if [[ $line = \#include* ]]; then + name=$( echo "$line" | cut -d'"' -f2) + preprocess_file "$name" + else + echo -e "$line" + fi + done < "$input_file" +} + +input_name="$1" +output_file="preprocessed-shaders/$input_name" + +preprocess_file "$input_name" > "$output_file" + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..c775653 --- /dev/null +++ b/readme.md @@ -0,0 +1,87 @@ +# TODO + +- imgui +- split up into commands: view, preview-colormap, save-image, save-video +- colormap invert option +- pass colormap values as command line option? +- see if the rendering can be made better, maybe dynamically adjust generation duration + +# Options + + +## subcommands + +- `view` +- `preview-colormaps` +- `save-image` +- `save-video` + +### `view` + +#### automaton options + +-[x] `--preset` `-p` `` +-[x] `--size` `-s` `x` +-[ ] `--birth` `-b` `` +-[ ] `--survive` `-u` `` +-[ ] `--max-age` `-a` `` +-[ ] `--starve-delay` `-d` `` +-[ ] `--starve-recover` `-r` `` +-[ ] `--initialise` `-i` `` + +#### display options +-[ ] `--display-size` `-S` `x` +-[ ] `--fullscreen` `-F` +-[ ] `--color-alive` `-A` `` +-[ ] `--color-dead` `-D` `` +-[ ] `--generation-duration` `-G` `` +-[ ] `--blend` `-B` + +## formats + +### `color` +possible values: +-[x] `#` +-[x] `rgb(uint,uint,uint)` +-[x] `lab(float,float,float)` +-[x] `luv(float,float,float)` +-[x] `oklab(float,float,float)` + +### `colormap` +TODO: this `:` syntax is inconsistent with the function-style syntax +-[x] `[,][,invert]` name of a predefined colormap, and optional scale, and optional invert flag +### `colormapdef`: +-[x] `` name of a predefined colormap +-[x] `map(, (float,float,float),...)` +-[x] `map([rgb,] )` + +### `colorlist-rgb`: +-[x] `` + +### `colorlist-color-rgb`: +-[x] `#` +-[x] `(,,)` + +### `color-or-colormap` +-[x] `` +-[x] `` + +### `colormap-scale` +-[x] `` +-[x] `inherent` +-[x] `global-max-age` only possible if max-age is set, otherwise fallback to `inherent` +-[x] `max-age` same as before + +### `init` +-[x] `rect(,)` +-[x] `square()` +-[x] `ellipse(,)` +-[x] `circle()` +-[x] `perlin(,)` scale and cutoff + +### `max-age` +-[x] `static()` +-[x] `radial(,)` center and corner max age, circular +-[x] `radial-fit(,)` center and corner max age, elliptic fit into size +-[x] `perlin-static(,,)` scale and min/max +-[x] `perlin-dynamic(, , , , )` scale, min, max, time-scale, time-step \ No newline at end of file diff --git a/shaders/automaton.comp b/shaders/automaton.comp new file mode 100644 index 0000000..99cb963 --- /dev/null +++ b/shaders/automaton.comp @@ -0,0 +1,104 @@ +#version 460 + +layout (local_size_x = 1, local_size_y = 1) in; + +uniform uvec2 dimensions; +uniform uint birth_rule; +uniform uint survive_rule; +uniform uint starve_delay; +uniform bool starve_recover; +uniform bool has_max_age; + + +layout (std430) restrict readonly buffer buffer_data_in +{ + uint data_in[]; +}; +layout (std430) restrict buffer buffer_data_out +{ + uint data_out[]; +}; + +#include "cellstate_common.glsl" + +bool check_condition(uint rule, uint count) { + return ((rule >> count) & uint(1)) == 1; +} + +void main() { + uint data_size = dimensions.x * dimensions.y; + uint neighbour_count = 0; + for (int ix = -1; ix < 2; ix++) { + for (int iy = -1; iy < 2; iy++) { + int cx = int(gl_GlobalInvocationID.x) + ix; + int cy = int(gl_GlobalInvocationID.y) + iy; + if (cx < 0) { + cx += int(dimensions.x); + } else if (cx >= dimensions.x) { + cx -= int(dimensions.x); + } + if (cy < 0) { + cy += int(dimensions.y); + } else if (cy >= dimensions.y) { + cy -= int(dimensions.y); + } + uint pos = cx + cy * dimensions.x; + neighbour_count += cellstate_from_data(data_in[pos]).alive ? 1 : 0; + } + } + + uint pos = gl_GlobalInvocationID.x + gl_GlobalInvocationID.y * dimensions.x; + CellState state = cellstate_from_data(data_in[pos]); + uint max_age = data_in[data_size + pos]; + + + if (state.alive) { + if (starve_recover) { + if (check_condition(birth_rule, neighbour_count)) { + state.starving = false; + } else { + if (state.starving) { + state.starve_duration = state.starve_duration + 1; + } else { + state.starving = true; + state.starve_duration = 0; + } + } + } else { + if (!check_condition(survive_rule, neighbour_count)) { + if (state.starving) { + state.starve_duration = state.starve_duration + 1; + } else { + state.starving = true; + state.starve_duration = 0; + } + } + } + + if (has_max_age && state.age >= max_age) { + state.alive = false; + state.starving = false; + state.age = 0; + state.starve_duration = 0; + } + + if (state.starving && state.starve_duration == starve_delay) { + state.starving = false; + state.alive = false; + state.age = 0; + state.starve_duration = 0; + } else { + state.age = state.age + 1; + } + } else { + if (check_condition(birth_rule, neighbour_count)) { + state.alive = true; + state.age = 0; + } + } + + data_out[pos] = cellstate_to_data(state); + + // propagate max-age + data_out[data_size + pos] = data_in[data_size + pos]; +} \ No newline at end of file diff --git a/shaders/automaton.frag b/shaders/automaton.frag new file mode 100644 index 0000000..ad6969a --- /dev/null +++ b/shaders/automaton.frag @@ -0,0 +1,108 @@ +#version 460 + +in vec2 coord; + +uniform bool preview_colormap; +uniform uvec2 dimensions; +uniform uint show_max_age; +uniform uint global_max_age; +uniform uint global_minimum_max_age; +uniform float blend_step; +uniform vec3 dead_color; +uniform bool dead_color_is_cielab; +uniform vec3 living_color; +uniform sampler1D living_colormap; +uniform bool living_use_colormap; +uniform bool living_colormap_invert; +uniform uint living_colormap_scale; +uniform bool living_colormap_scale_is_max_age; +uniform bool living_color_is_cielab; + +out vec3 color; + +layout (std430) restrict readonly buffer buffer_data_from +{ + uint data_from[]; +}; + +layout (std430) restrict readonly buffer buffer_data_to +{ + uint data_to[]; +}; + +#include "cellstate_common.glsl" +#include "color_conversion.glsl" + +vec3 blend_color(vec3 from, vec3 to, float step) { + return vec3( + from.x * (1. - step) + to.x * step, + from.y * (1. - step) + to.y * step, + from.z * (1. - step) + to.z * step + ); +} + +// TODO: support cielab, cieluv and oklab? +vec3 get_color(CellState state, uint living_colormap_scale_) { + vec3 color; + if (state.alive) { + if (living_use_colormap) { + float pos = float(state.age) / float(living_colormap_scale_); + if (living_colormap_invert) { + pos = 1. - pos; + } + return texture(living_colormap, pos).rgb; + } else { + return living_color; + } + } else { + return dead_color; + } +} + + +void main() { + uint x = uint(float(dimensions.x) * (coord.x + 1.) / 2.); + uint y = uint(float(dimensions.y) * (coord.y + 1.) / 2.); + uint pos = x + y * dimensions.x; + + uint max_age = data_from[dimensions.x * dimensions.y + pos]; + float max_age_val = float(max_age - global_minimum_max_age) / float(global_max_age); + + CellState state_from = cellstate_from_data(data_from[pos]); + CellState state_to = cellstate_from_data(data_to[pos]); + uint living_colormap_scale_from; + uint living_colormap_scale_to; + if (living_colormap_scale_is_max_age) { + living_colormap_scale_from = data_from[dimensions.x * dimensions.y + pos]; + living_colormap_scale_to = data_to[dimensions.x * dimensions.y + pos]; + } else { + living_colormap_scale_from = living_colormap_scale; + living_colormap_scale_to = living_colormap_scale; + } + + vec3 color_from = get_color(state_from, living_colormap_scale_from); + vec3 color_to = get_color(state_to, living_colormap_scale_to); + + vec3 color_lab = blend_color(color_from, color_to, blend_step); + vec3 color_rgb = convert_cielab_to_rgb(color_lab); + + if (show_max_age == 0) { + color = color_rgb; + } else if (show_max_age == 1) { + color = vec3(max_age_val,max_age_val,max_age_val); + } else if (show_max_age == 2) { + float r = color_rgb.r * .1 + max_age_val * .9; + float g = color_rgb.g * .1 + max_age_val * .9; + float b = color_rgb.b * .1 + max_age_val * .9; + color = vec3(r,g,b); + } + + if (preview_colormap) { + color = convert_cielab_to_rgb(texture(living_colormap, (coord.x+1.)/2.).rgb); + } + +} +//void main(){ +//// color = texture(living_colormap, (coord.x+1.)/2.).rgb; +// color = convert_cielab_to_rgb(texture(living_colormap, (coord.x+1.)/2.).rgb); +//} \ No newline at end of file diff --git a/shaders/automaton.vert b/shaders/automaton.vert new file mode 100644 index 0000000..df04e62 --- /dev/null +++ b/shaders/automaton.vert @@ -0,0 +1,9 @@ +#version 460 + +layout (location = 0) in vec3 vertexPosition_modelspace; +out vec2 coord; +void main() { + gl_Position.xyz = vertexPosition_modelspace; + gl_Position.w = 1.0; + coord.xy = vertexPosition_modelspace.xy; +} \ No newline at end of file diff --git a/shaders/cellstate_common.glsl b/shaders/cellstate_common.glsl new file mode 100644 index 0000000..ab12c7a --- /dev/null +++ b/shaders/cellstate_common.glsl @@ -0,0 +1,22 @@ +struct CellState { + bool alive; + bool starving; + uint age; // max 65025 + uint starve_duration; // max 255 +}; + +CellState cellstate_from_data(uint data) { + bool alive = (data & uint(0x1)) == 1; + bool starving = ((data >> 1) & uint(0x1)) == 1; + uint age = (data >> 8) & uint(0xFFFF); + uint starve_duration = (data >> 24) & uint(0xFF); + + return CellState(alive, starving, age, starve_duration); +} + +uint cellstate_to_data(CellState cellstate) { + return (cellstate.starve_duration << 24) + | (cellstate.age << 8) + | (cellstate.starving ? uint(0x2) : uint(0x0)) + | (cellstate.alive ? uint(0x1) : uint(0x0)); +} diff --git a/shaders/color_conversion.glsl b/shaders/color_conversion.glsl new file mode 100644 index 0000000..662b8e8 --- /dev/null +++ b/shaders/color_conversion.glsl @@ -0,0 +1,80 @@ +vec3 convert_ciexyz_to_linear_rgb(vec3 xyz) { + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html sRGB M^-1 matrix + // mat3 M = mat3( + // 3.2404542, -1.5371385, -0.4985314, + // -0.9692660, 1.8760108, 0.0415560, + // 0.0556434, -0.2040259, 1.0572252 + // ); + // TODO: figure out which is correct, GLSL uses column-major for matrices + + mat3 M = mat3( + 3.2404542, -0.9692660, 0.0556434, + -1.5371385, 1.8760108, -0.2040259, + -0.4985314, 0.0415560, 1.0572252 + ); + return M * xyz; +} + + +vec3 convert_cielab_to_xyz(vec3 cielab, vec3 ref_white) { + float Xn = ref_white.x; + float Yn = ref_white.y; + float Zn = ref_white.z; + + float L = cielab.x; + float a = cielab.y; + float b = cielab.z; + + float epsilon = 216./24289.; + float kappa = 24389./27.; + + float fy = (L + 16.) / 116.; + float fz = fy - b / 200.; + float fx = a / 500. + fy; + + float xr; + float yr; + float zr; + + if (pow(fx, 3.) > epsilon) { + xr = pow(fx, 3.); + } else { + xr = (116. * fx - 16.) / kappa; + } + + if (L > kappa * epsilon) { + yr = pow((L + 16.) / 116., 3.); + } else { + yr = L / kappa; + } + + if (pow(fz, 3.) > epsilon) { + zr = pow(fz, 3.); + } else { + zr = (116. * fz - 16.) / kappa; + } + + float X = xr * Xn; + float Y = yr * Yn; + float Z = zr * Zn; + return vec3(X, Y, Z); +} + + +vec3 convert_cielab_to_rgb(vec3 cielab) { + // using D65 + float Xn = .950489; + float Yn = 1.; + float Zn = 1.0888; + vec3 xyz = convert_cielab_to_xyz(cielab, vec3(Xn, Yn, Zn)); + vec3 linear_rgb = convert_ciexyz_to_linear_rgb(xyz); + return linear_rgb; +} + +vec3 convert_cieluv_to_rgb(vec3 cieluv) { + return cieluv;// TODO +} + +vec3 convert_oklab_to_rgb(vec3 oklab) { + return oklab;// TODO +} \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..1284dc7 --- /dev/null +++ b/shell.nix @@ -0,0 +1,7 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = [ + pkgs.clang_16 + ]; +} diff --git a/src/argparser.h b/src/argparser.h new file mode 100644 index 0000000..1ec2b93 --- /dev/null +++ b/src/argparser.h @@ -0,0 +1,1679 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace argparser { +namespace errors { +namespace internal { +inline std::string quote_string(const std::string &str) { + std::stringstream ss{}; + ss << std::quoted(str); + return ss.str(); +} +}// namespace internal + +class runtime_error : public std::runtime_error { +public: + template + explicit runtime_error(Ts... args) : std::runtime_error(args...) {} +}; + +class logic_error : public std::logic_error { +public: + template + explicit logic_error(Ts... args) : std::logic_error(args...) {} +}; + +class missing_option_error : public runtime_error { +public: + explicit missing_option_error(std::string option_name) : runtime_error(std::format("missing required option {}", option_name)) {} +}; + +class missing_argument_error : public runtime_error { +public: + explicit missing_argument_error(std::string arg_name) : runtime_error(std::format("missing argument {}", arg_name)) {} +}; + +class unknown_option_error : public runtime_error { +public: + explicit unknown_option_error(std::string option_name) : runtime_error(std::format("unknown option {}", option_name)) {} +}; + +class wrong_option_count_error : public runtime_error { +public: + wrong_option_count_error(const std::string &option_name, std::optional min, std::optional max, unsigned int actual) + : runtime_error(make_message(option_name, min, max, actual)) {} + +private: + static std::string make_message(std::string option_name, std::optional min, std::optional max, unsigned int actual) { + if (min != std::nullopt && max != std::nullopt) { + return std::format("option {} was provided {} times, but is required at least {} times and at most {} times", option_name, actual, min.value(), max.value()); + } else if (min != std::nullopt) { + return std::format("option {} was provided {} times, but is required at least {} times", option_name, actual, min.value()); + } else { + return std::format("option {} was provided {} times, but is required at most {} times", option_name, actual, max.value()); + } + } +}; + +class missing_option_value_error : public runtime_error { +public: + explicit missing_option_value_error(std::string option_name) : runtime_error(std::format("missing value for option {}", option_name)) {} +}; + +class not_enough_arguments_error : public runtime_error { +public: + explicit not_enough_arguments_error(size_t min_required) : runtime_error(std::format("not enough arguments, need at least {}", min_required)) {} +}; + +class too_many_arguments_error : public runtime_error { +public: + explicit too_many_arguments_error() : runtime_error("too many arguments") {} +}; + +class invalid_option_value_error : public runtime_error { +public: + explicit invalid_option_value_error(std::string option_name) : runtime_error(std::format("invalid value for option {}", option_name)) {} +}; + +class unexpected_option_value_error : public runtime_error { +public: + explicit unexpected_option_value_error(std::string option_name) : runtime_error(std::format("unexpected value for option {}", option_name)) {} +}; + +class type_parsing_error : public runtime_error { +public: + type_parsing_error(std::string type_name, std::string input, size_t error_pos, std::string message) + : runtime_error(std::format("error parsing type {} at position {} in \"{}\": {}", type_name, error_pos, internal::quote_string(input), message)), + type_name(type_name), input(std::move(input)), error_pos(error_pos) {} + + const std::string type_name; + const std::string input; + const int error_pos; +}; + +template +class validation_error : public runtime_error { +public: + validation_error(std::string type_name, T value, std::string parsed_str) + : runtime_error(std::format("validation failed for type {} with value {} ({})", type_name, make_value_str(value), parsed_str)) {} + +private: + std::string make_value_str(T val) { + if constexpr (std::is_convertible_v or + std::is_convertible_v) { + return val; + } else if constexpr (std::is_arithmetic_v) { + return std::to_string(val); + } else { + return ""; + } + } +}; + +class ambiguous_parse_error : public type_parsing_error { +public: + explicit ambiguous_parse_error(std::string type_name, std::string input, size_t error_pos, std::vector possible_types) + : type_parsing_error(std::move(type_name), input, error_pos, make_message(std::move(possible_types))) {} + +private: + static std::string make_message(std::vector possible_types) { + std::string message = "ambiguity between "; + for (auto it = possible_types.begin(); it != possible_types.end(); it++) { + if (std::next(it) == possible_types.end()) { + message += " and " + *it; + } else { + message += ", " + *it; + } + } + return message; + } +}; + +class invalid_option_name_error : public logic_error { +public: + explicit invalid_option_name_error(std::string option_name) : logic_error(std::format("invalid option name {}", option_name)) {} +}; + +class duplicate_option_name : public logic_error { +public: + explicit duplicate_option_name(std::string option_name) : logic_error(std::format("option with name {} already exists", option_name)) {} +}; + +class duplicate_argument_name : public logic_error { +public: + explicit duplicate_argument_name(std::string arg_name) : logic_error(std::format("argument with name {} already exists", arg_name)) {} +}; + +class empty_enum_map_error : public logic_error { +public: + explicit empty_enum_map_error(std::string type_name) : logic_error(std::format("no enum values for type {}", type_name)) {} +}; +}// namespace errors + +namespace internal { +constexpr std::string_view whitespace = " \f\n\r\t\v"; +enum class parser_allow_undelimited { + None = 0, + Comma = 1, + Parenthesis = 2, + Brackets = 4, + Any = 7, +}; + +constexpr parser_allow_undelimited operator|(parser_allow_undelimited a, parser_allow_undelimited b) { + return static_cast(static_cast>(a) | static_cast>(b)); +} + +constexpr parser_allow_undelimited operator&(parser_allow_undelimited a, parser_allow_undelimited b) { + return static_cast(static_cast>(a) & static_cast>(b)); +} +enum class parse_opt { + None = 0, + BareString = 1, + SingleQuotedString = 2, + DoubleQuotedString = 4, + AnyString = 7, + TrimString = 8, + TrimBareString = 16, +}; + +constexpr parse_opt operator|(parse_opt a, parse_opt b) { + return static_cast(static_cast>(a) | static_cast>(b)); +} + +constexpr parse_opt operator&(parse_opt a, parse_opt b) { + return static_cast(static_cast>(a) & static_cast>(b)); +} + +template +constexpr bool enum_flag_contains(T a, T b) { + return static_cast>(a & b) != 0; +} + +template +using string_map = std::map>; +}// namespace internal + +class parse_result { + internal::string_map opts{}; + internal::string_map args{}; + std::vector remaining_args{}; + +public: + void set_opt(const std::string &name, std::any value) { + opts[name] = std::move(value); + } + + [[nodiscard]] std::any get_opt(const std::string &name) const { + if (opts.find(name) != opts.end()) { + return opts.at(name); + } else { + return {}; + } + } + + [[nodiscard]] bool has_opt(const std::string &name) const { + return opts.find(name) != opts.end(); + } + + void set_arg(const std::string &name, std::any value) { + args[name] = std::move(value); + } + + [[nodiscard]] std::any get_arg(const std::string &name) const { + if (args.find(name) != args.end()) { + return args.at(name); + } else { + return {}; + } + } + + [[nodiscard]] bool has_arg(const std::string &name) const { + auto argit = args.find(name); + if (argit == args.end()) + return false; + return argit != args.end() && argit->second.has_value(); + } + + void set_remaining(std::vector remaining) { + remaining_args = std::move(remaining); + } + + [[nodiscard]] std::vector remaining() const { + return remaining_args; + } +}; + +class type { +public: + virtual ~type() = default; + + [[nodiscard]] std::string get_name() const { + return name; + } + +protected: + explicit type(std::string name) : name(std::move(name)) {} + + std::string name; +}; + +using type_handle = std::shared_ptr; +template +using validator_fn = std::function; + +template +class type_impl : public type, public std::enable_shared_from_this> { +public: + [[nodiscard]] T parse(const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited = internal::parser_allow_undelimited::None) const { + auto val = this->do_parse(begin, end, parse_end, allow_undelimited); + if (validator != nullptr && !validator(val)) { + throw errors::validation_error(name, val, std::string(begin, parse_end)); + } + return val; + } + + std::shared_ptr> validate(validator_fn fn) { + validator = fn; + return this->shared_from_this(); + } + +protected: + explicit type_impl(std::string name) : type(std::move(name)) {} + + validator_fn validator = nullptr; + +private: + virtual T do_parse(const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited) const = 0; +}; + +template +using type_handle_impl = std::shared_ptr>; + +class arg { +public: + explicit arg(std::string name) : name(std::move(name)) {} + + virtual ~arg() = default; + + void parse(const std::string &input, parse_result &pr) const { + do_parse(input, pr); + } + + [[nodiscard]] bool has_parsed_enough(parse_result &pr) const { + return this->get_has_parsed_enough(pr); + } + + [[nodiscard]] bool can_parse_more(parse_result &pr) const { + return this->get_can_parse_more(pr); + } + + [[nodiscard]] std::string get_name() const { + return name; + } + +protected: + const std::string name; + + template + static T parse_single_value(std::string input, const type_handle_impl &type) { + const char *parse_end; + auto begin = &*input.begin(); + auto end = &*input.end(); + auto val = type->parse(begin, end, parse_end, internal::parser_allow_undelimited::Any); + if (parse_end != end) { + throw errors::type_parsing_error(type->get_name(), std::string(begin, end), parse_end - begin, "unexpected input"); + } + return val; + } + +private: + virtual void do_parse(std::string input, parse_result &pr) const = 0; + [[nodiscard]] virtual bool get_has_parsed_enough(parse_result &pr) const = 0; + [[nodiscard]] virtual bool get_can_parse_more(parse_result &pr) const = 0; +}; + +using arg_handle = std::shared_ptr; + +namespace internal { +template +using parser_func = std::function; +} + +template +class basic_type : public type_impl { +public: + basic_type(std::string name, internal::parser_func parser) : type_impl(std::move(name)), parser(std::move(parser)) {} + +private: + T do_parse(const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited) const override { + try { + const char *n_parse_end; + auto val = parser(begin, end, n_parse_end, allow_undelimited); + parse_end = n_parse_end; + return val; + } catch (std::runtime_error &e) { + throw errors::type_parsing_error(this->name, std::string(begin, end), 0, e.what()); + } catch (...) { + throw errors::type_parsing_error(this->name, std::string(begin, end), 0, "unknown parse error"); + } + } + + internal::parser_func parser; +}; + +namespace internal { +template +class automatic_parser { +public: + static parser_func make_parser(const std::string &name) { + return [name](const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited) { + T val; + auto pos = begin; + while (pos < end && isspace(*pos)) pos++; + auto res = std::from_chars(pos, end, val); + if (res.ec == std::errc{}) { + parse_end = res.ptr; + return val; + } else { + throw errors::type_parsing_error(name, std::string(begin, end), pos - begin, "invalid number"); + } + }; + } +}; + +constexpr parse_opt string_parse_opt_with_default(parse_opt opt) { + if ((opt & parse_opt::AnyString) == parse_opt::None) { + return opt | parse_opt::AnyString; + } else { + return opt; + } +} + +constexpr bool string_parser_enable_bare(parse_opt opt) { + return enum_flag_contains(string_parse_opt_with_default(opt), parse_opt::BareString); +} + +constexpr bool string_parser_enable_single_quoted(parse_opt opt) { + return enum_flag_contains(string_parse_opt_with_default(opt), parse_opt::SingleQuotedString); +} + +constexpr bool string_parser_enable_double_quoted(parse_opt opt) { + return enum_flag_contains(string_parse_opt_with_default(opt), parse_opt::DoubleQuotedString); +} + +template +class automatic_parser { + static std::string trim_string(const std::string &str) { + auto start = str.find_first_not_of(whitespace); + auto end = str.find_last_not_of(whitespace); + return str.substr(start == std::string::npos ? 0 : start, end == std::string::npos ? 0 : end); + } + +public: + static parser_func make_parser(const std::string &name) { + return [name](const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited) { + auto str = std::string_view(begin, end); + if (str.length() == 0) { + if (parse_opt == parse_opt::None || enum_flag_contains(parse_opt, parse_opt::BareString)) { + parse_end = begin; + return std::string(""); + } else { + throw errors::type_parsing_error(name, std::string(""), 0, "unexpected empty input"); + } + } + if (str[0] == '\'' && string_parser_enable_single_quoted(parse_opt)) { + auto ss = std::stringstream(std::string(str)); + std::string val; + ss >> std::quoted(val, '\'', '\\'); + auto len = ss.tellg(); + parse_end = begin + len; + if (enum_flag_contains(parse_opt, parse_opt::TrimString)) { + return trim_string(val); + } else { + return val; + } + } + if (str[0] == '"' && string_parser_enable_double_quoted(parse_opt)) { + auto ss = std::stringstream(std::string(str)); + std::string val; + ss >> std::quoted(val, '"', '\\'); + auto len = ss.tellg(); + parse_end = begin + len; + if (enum_flag_contains(parse_opt, parse_opt::TrimString)) { + return trim_string(val); + } else { + return val; + } + } + if (string_parser_enable_bare(parse_opt)) { + std::string illegal_characters = "\"'"; + if (!internal::enum_flag_contains(allow_undelimited, internal::parser_allow_undelimited::Comma)) { + illegal_characters += ","; + } + if (!internal::enum_flag_contains(allow_undelimited, internal::parser_allow_undelimited::Parenthesis)) { + illegal_characters += "()"; + } + if (!internal::enum_flag_contains(allow_undelimited, internal::parser_allow_undelimited::Brackets)) { + illegal_characters += "[]"; + } + auto pos = str.find_first_of(illegal_characters); + if (pos == std::string_view::npos) { + parse_end = end; + } else { + parse_end = begin + pos; + } + std::string val{begin, parse_end}; + if (enum_flag_contains(parse_opt, parse_opt::TrimString) || enum_flag_contains(parse_opt, parse_opt::TrimBareString)) { + return trim_string(val); + } else { + return val; + } + } + throw errors::type_parsing_error(name, std::string(begin, end), 0, "failed to parse string"); + }; + } +}; + +template +parser_func make_enum_parser(const std::string &name, internal::string_map values) { + if (values.size() == 0) { + throw errors::empty_enum_map_error(name); + } + return [name, values](const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited) { + auto input = std::basic_string_view(begin, end); + auto start = input.find_first_not_of(whitespace); + if (start == std::string::npos) { + input = ""; + } else { + input = std::basic_string_view(begin + start, end); + } + T value; + size_t value_len = 0; + for (const auto &[str, val]: values) { + if (str.length() <= input.length()) { + if (input.starts_with(str)) { + if (str.length() > value_len) { + value = val; + value_len = str.length(); + } + } + } + } + if (value_len == 0) { + std::string expected_values = values.begin()->first; + for (auto it = std::next(values.begin()); it != values.end(); it++) { + if (std::next(it) != values.end()) { + expected_values += ", " + it->first; + } else { + expected_values += " or " + it->first; + } + } + throw errors::type_parsing_error(name, std::string(begin, end), 0, std::format("expected {}", expected_values)); + } else { + parse_end = begin + value_len; + return value; + } + }; +} +}// namespace internal + +namespace internal::distinct_types_impl { +template +struct UniqueTypes; + +template typename X, typename... Ts> +struct UniqueTypes> { + using type = X; +}; + +template typename X, typename... Ts, typename T, typename... Us> +struct UniqueTypes, T, Us...> { + using type = typename UniqueTypes< + typename std::conditional< + std::disjunction...>::value, + X, + X>::type, + Us...>::type; +}; + +template typename X, typename... Ts> +struct Distinct { + using type = typename UniqueTypes, Ts...>::type; +}; +}// namespace internal::distinct_types_impl + +namespace internal { +template +using distinct_types_variant = typename internal::distinct_types_impl::Distinct::type; +template +using distinct_types_tuple = typename internal::distinct_types_impl::Distinct::type; +template +using if_single_type = std::enable_if_t>::value == 1, T>; +template +using if_not_single_type = std::enable_if_t<(std::tuple_size>::value > 1), T>; +template +using single_type_conditional = std::conditional<(std::tuple_size>::value == 1), T, F>; +template +using single_type = if_single_type>::type, Ts...>; +}// namespace internal + +template +class list_type : public type_impl> { +public: + list_type(std::string name, type_handle_impl element_type) : type_impl>(std::move(name)), element_type(element_type) {} + +private: + std::vector do_parse(const char *begin, const char *end, const char *&parse_end, internal::parser_allow_undelimited allow_undelimited) const override { + auto cur_pos = begin; + std::vector values{}; + bool is_delimited; + if (*cur_pos == '[') { + is_delimited = true; + cur_pos++; + } else { + if (!internal::enum_flag_contains(allow_undelimited, internal::parser_allow_undelimited::Comma)) { + throw errors::type_parsing_error(this->name, std::string(begin, end), cur_pos - begin, "expected '['"); + } else { + is_delimited = false; + } + } + if (cur_pos >= end) { + throw errors::type_parsing_error(this->name, std::string(begin, end), end - begin, "unexpected end of input"); + } + internal::parser_allow_undelimited sub_parse_allow_undelimited = internal::parser_allow_undelimited::Parenthesis; + if (!is_delimited) { + sub_parse_allow_undelimited &allow_undelimited; + } + while (true) { + const char *this_parse_end; + auto val = element_type->parse(cur_pos, end, this_parse_end, sub_parse_allow_undelimited); + values.push_back(val); + cur_pos = this_parse_end; + while (std::isspace(*cur_pos) && cur_pos <= end) + cur_pos++; + if (cur_pos >= end) { + if (!is_delimited) { + break; + } else { + throw errors::type_parsing_error(this->name, std::string(begin, end), end - begin, "unexpected end of input"); + } + } + if (*cur_pos == ',') { + cur_pos++; + auto s = std::string_view(cur_pos, end); + auto close_bracket_pos = s.find_first_of(']'); + if (s.find_first_not_of(internal::whitespace) == close_bracket_pos) { + break; + } + } else if (!is_delimited || *cur_pos == ']') { + break; + } else { + throw errors::type_parsing_error(this->name, std::string(begin, end), end - begin, "expected , or ]"); + } + } + if (is_delimited) { + if (*cur_pos != ']') { + throw errors::type_parsing_error(this->name, std::string(begin, end), end - begin, "unexpected end of input"); + } else { + cur_pos++; + } + } + parse_end = cur_pos; + return values; + } + + type_handle_impl element_type; +}; + +class option { +public: + explicit option(std::string name) : name(std::move(name)) {} + + virtual ~option() = default; + + void parse(std::optional arg, std::any &val) { + return this->do_parse(std::move(arg), val); + } + + [[nodiscard]] std::string get_name() { + return name; + } + + [[nodiscard]] virtual bool consumes_value() const = 0; + virtual void validate(const parse_result &res) const = 0; + +protected: + const std::string name; + +private: + virtual void do_parse(std::optional arg, std::any &val) = 0; +}; + +using option_handle = std::shared_ptr