diff --git a/.github/workflows/system-packages.yml b/.github/workflows/system-packages.yml index 8b5fc57..86b806c 100644 --- a/.github/workflows/system-packages.yml +++ b/.github/workflows/system-packages.yml @@ -9,10 +9,13 @@ on: - "**.cpp" - "**.hpp" - "**.hxx" + - "**.vert" + - "**.frag" - "meson.build" - "meson_options.txt" - "**/meson.build" - "subprojects/*.wrap" + - "subprojects/packagefiles/**" - ".github/workflows/system-packages.yml" pull_request: paths: @@ -21,10 +24,13 @@ on: - "**.cpp" - "**.hpp" - "**.hxx" + - "**.vert" + - "**.frag" - "meson.build" - "meson_options.txt" - "**/meson.build" - "subprojects/*.wrap" + - "subprojects/packagefiles/**" - ".github/workflows/system-packages.yml" jobs: @@ -133,6 +139,39 @@ jobs: - name: Build run: meson compile -C build/ + - name: Verify Qt-only frontend + if: matrix.build_type == 'debug' + env: + CC: ccache ${{ matrix.platform.cc }} + CXX: ccache ${{ matrix.platform.cxx }} + run: | + meson setup build-qt-only/ \ + --buildtype=debug \ + --wrap-mode=forcefallback \ + -Dtesting=false -Dexamples=false \ + -Dqt=true -Dglfw=false -Dimgui=false + ninja -C build-qt-only/ \ + visualization/libbalsaVisualization.so.p/src_qt_vulkan_film.cpp.o \ + visualization/libbalsaVisualization.so.p/src_qt_vulkan_window.cpp.o \ + visualization/tools/mesh_viewer_qt.p/mesh_viewer_qt.cpp.o + + - name: Verify GLFW ImGui frontend + if: matrix.build_type == 'debug' + env: + CC: ccache ${{ matrix.platform.cc }} + CXX: ccache ${{ matrix.platform.cxx }} + run: | + meson setup build-glfw-imgui/ \ + --buildtype=debug \ + --wrap-mode=forcefallback \ + -Dtesting=false -Dexamples=false \ + -Dqt=false -Dglfw=true -Dimgui=true + ninja -C build-glfw-imgui/ \ + visualization/libbalsaVisualization.so.p/src_glfw_vulkan_film.cpp.o \ + visualization/libbalsaVisualization.so.p/src_glfw_vulkan_window.cpp.o \ + visualization/libbalsaVisualization.so.p/src_vulkan_imgui_integration.cpp.o \ + visualization/tools/mesh_viewer_glfw.p/mesh_viewer_glfw.cpp.o + - name: ccache stats run: ccache -s diff --git a/meson_options.txt b/meson_options.txt index 2cc7db2..bea2486 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -6,10 +6,10 @@ option('openvdb', type : 'boolean', value : false, description : 'Build openvdb option('json', type : 'boolean', value : true, description : 'Use json') option('protobuf', type : 'boolean', value : false, description : 'Use protobuf') option('partio', type : 'boolean', value : false, description : 'Use partio') -option('imgui', type : 'boolean', value : true, description : 'Use imgui') +option('imgui', type : 'boolean', value : true, description : 'Build the ImGui UI frontend (currently requires GLFW)') option('alembic', type : 'boolean', value : false, description : 'Use alembic') option('perfetto', type : 'boolean', value : false, description : 'Use perfetto') option('visualization', type : 'boolean', value : true, description : 'Build visualization library') -option('glfw', type : 'boolean', value : true, description : 'Use glfw') -option('qt', type : 'boolean', value : true, description : 'Use qt') +option('glfw', type : 'boolean', value : true, description : 'Build the GLFW window backend') +option('qt', type : 'boolean', value : true, description : 'Build the Qt window and widget frontend') diff --git a/subprojects/.gitignore b/subprojects/.gitignore index 8f7a5ff..5bbbfc5 100644 --- a/subprojects/.gitignore +++ b/subprojects/.gitignore @@ -1,5 +1,8 @@ */ !patches/ +!packagefiles/ +!packagefiles/colormap_shaders/ +!packagefiles/colormap_shaders/meson.build # Auto-generated wrap-redirects from transitive deps lua.wrap diff --git a/subprojects/colormap_shaders.wrap b/subprojects/colormap_shaders.wrap index ce5cf57..1896879 100644 --- a/subprojects/colormap_shaders.wrap +++ b/subprojects/colormap_shaders.wrap @@ -1,6 +1,7 @@ [wrap-git] url = https://github.com/mtao/colormap-shaders.git revision = 142a208adf287335d53d368aa5dea6ec708a8aef +patch_directory = colormap_shaders [provide] colormap_shaders = colormap_shaders_dep diff --git a/subprojects/packagefiles/colormap_shaders/meson.build b/subprojects/packagefiles/colormap_shaders/meson.build new file mode 100644 index 0000000..55db239 --- /dev/null +++ b/subprojects/packagefiles/colormap_shaders/meson.build @@ -0,0 +1,9 @@ +project('colormap_shaders', 'cpp', version: '0.0') + +colormap_shader_dir = meson.current_source_dir() / 'shaders' / 'glsl' +colormap_shaders_dep = declare_dependency( + include_directories: include_directories('include'), + variables: {'shader_dir': colormap_shader_dir}, +) + +meson.override_dependency('colormap_shaders', colormap_shaders_dep) diff --git a/visualization/examples/meson.build b/visualization/examples/meson.build index ca8d294..a767146 100644 --- a/visualization/examples/meson.build +++ b/visualization/examples/meson.build @@ -1,5 +1,16 @@ -test_example_scene_dep = static_library('vulkan_scene', 'vulkan_scene.cpp', dependencies: [visualization_dep]) -executable('vulkan_window', 'vulkan_window.cpp', 'vulkan_scene.cpp', dependencies: [visualization_dep]) -#executable('test_vulkan_window', 'test_vulkan_window.cpp', 'example_vulkan_scene.cpp', dependencies: [visualization_dep]) -executable('vulkan_window_glfw', 'vulkan_window_glfw.cpp', dependencies: [visualization_dep], link_with: [test_example_scene_dep]) +# The existing example scene includes an ImGui overlay, so only build it for +# the GLFW + ImGui frontend. Qt-only examples should not acquire ImGui. +if get_option('glfw') and get_option('imgui') + test_example_scene_dep = static_library( + 'vulkan_scene', + 'vulkan_scene.cpp', + dependencies: [visualization_dep], + ) + executable( + 'vulkan_window_glfw', + 'vulkan_window_glfw.cpp', + dependencies: [visualization_dep], + link_with: [test_example_scene_dep], + ) +endif diff --git a/visualization/examples/vulkan_scene.cpp b/visualization/examples/vulkan_scene.cpp index 8a5da77..2b39584 100644 --- a/visualization/examples/vulkan_scene.cpp +++ b/visualization/examples/vulkan_scene.cpp @@ -2,6 +2,7 @@ #else #include #include +#include #include "vulkan_scene.hpp" #include #pragma GCC diagnostic push @@ -20,13 +21,17 @@ class TriangleShader : public Shader { template std::vector TriangleShader::vert_spirv() const { - const static std::string fname = ":/glsl/triangle.vert"; - return AbstractShader::compile_glsl_from_path(fname, AbstractShader::ShaderType::Vertex); + const auto source = builtin_shader_library().find("examples/triangle/vertex"); + return AbstractShader::compile_glsl( + source ? std::string_view{*source} : std::string_view{}, + AbstractShader::ShaderType::Vertex); } template std::vector TriangleShader::frag_spirv() const { - const static std::string fname = ":/glsl/triangle.frag"; - return AbstractShader::compile_glsl_from_path(fname, AbstractShader::ShaderType::Fragment); + const auto source = builtin_shader_library().find("examples/triangle/fragment"); + return AbstractShader::compile_glsl( + source ? std::string_view{*source} : std::string_view{}, + AbstractShader::ShaderType::Fragment); } }// namespace balsa::visualization::shaders diff --git a/visualization/include/balsa/visualization/colormap_list.hpp b/visualization/include/balsa/visualization/colormap_list.hpp index 546cf1e..5742d26 100644 --- a/visualization/include/balsa/visualization/colormap_list.hpp +++ b/visualization/include/balsa/visualization/colormap_list.hpp @@ -1,143 +1,17 @@ -#if !defined(BALSA_VISUALIZATION_COLORMAP_LIST_HPP) -#define BALSA_VISUALIZATION_COLORMAP_LIST_HPP +#pragma once -#include -#include +#include "balsa/visualization/shaders/embedded_sources.hpp" namespace balsa::visualization { -// ── Colormap names ────────────────────────────────────────────────── -// -// Canonical list of available colormaps from the colormap_shaders -// subproject. Shared by both ImGui and Qt UI panels so the list -// stays in sync. Each name corresponds to a Qt resource alias -// at ":/colormap-shaders/.frag". - -inline constexpr const char *k_colormap_names[] = { - // MATLAB family - "MATLAB_jet", - "MATLAB_parula", - "MATLAB_hot", - "MATLAB_cool", - "MATLAB_spring", - "MATLAB_summer", - "MATLAB_autumn", - "MATLAB_winter", - "MATLAB_bone", - "MATLAB_copper", - "MATLAB_pink", - "MATLAB_hsv", - // transform family - "transform_rainbow", - "transform_seismic", - "transform_hot_metal", - "transform_grayscale_banded", - "transform_space", - "transform_supernova", - "transform_ether", - "transform_malachite", - "transform_morning_glory", - "transform_peanut_butter_and_jerry", - "transform_purple_haze", - "transform_rose", - "transform_saturn", - "transform_lava_waves", - "transform_apricot", - "transform_carnation", - // IDL ColorBrewer (sequential / diverging) - "IDL_CB-Blues", - "IDL_CB-Greens", - "IDL_CB-Greys", - "IDL_CB-Oranges", - "IDL_CB-Purples", - "IDL_CB-Reds", - "IDL_CB-BuGn", - "IDL_CB-BuPu", - "IDL_CB-GnBu", - "IDL_CB-OrRd", - "IDL_CB-PuBu", - "IDL_CB-PuBuGn", - "IDL_CB-PuOr", - "IDL_CB-PuRd", - "IDL_CB-RdBu", - "IDL_CB-RdGy", - "IDL_CB-RdPu", - "IDL_CB-RdYiBu", - "IDL_CB-RdYiGn", - "IDL_CB-BrBG", - "IDL_CB-PiYG", - "IDL_CB-PRGn", - "IDL_CB-Spectral", - "IDL_CB-YIGn", - "IDL_CB-YIGnBu", - "IDL_CB-YIOrBr", - // IDL ColorBrewer (qualitative) - "IDL_CB-Accent", - "IDL_CB-Dark2", - "IDL_CB-Paired", - "IDL_CB-Pastel1", - "IDL_CB-Pastel2", - "IDL_CB-Set1", - "IDL_CB-Set2", - "IDL_CB-Set3", - // IDL classic - "IDL_Rainbow", - "IDL_Rainbow_2", - "IDL_Rainbow_18", - "IDL_Rainbow+Black", - "IDL_Rainbow+White", - "IDL_Blue-Red", - "IDL_Blue-Red_2", - "IDL_Blue-White_Linear", - "IDL_Blue-Green-Red-Yellow", - "IDL_Blue-Pastel-Red", - "IDL_Blue_Waves", - "IDL_Green-Pink", - "IDL_Green-Red-Blue-White", - "IDL_Green-White_Exponential", - "IDL_Green-White_Linear", - "IDL_Red-Purple", - "IDL_Red_Temperature", - "IDL_Black-White_Linear", - "IDL_16_Level", - "IDL_Beach", - "IDL_Eos_A", - "IDL_Eos_B", - "IDL_Hardcandy", - "IDL_Haze", - "IDL_Hue_Sat_Lightness_1", - "IDL_Hue_Sat_Lightness_2", - "IDL_Hue_Sat_Value_1", - "IDL_Hue_Sat_Value_2", - "IDL_Mac_Style", - "IDL_Nature", - "IDL_Ocean", - "IDL_Pastels", - "IDL_Peppermint", - "IDL_Plasma", - "IDL_Prism", - "IDL_Purple-Red+Stripes", - "IDL_Standard_Gamma-II", - "IDL_Steps", - "IDL_Stern_Special", - "IDL_Volcano", - "IDL_Waves", - // Other - "gnuplot", - "kbinani_altitude", -}; - -inline constexpr int k_colormap_count = static_cast( - sizeof(k_colormap_names) / sizeof(k_colormap_names[0])); - -// Find the index of a colormap name, or -1 if not found. -inline int find_colormap_index(const std::string &name) { - for (int i = 0; i < k_colormap_count; ++i) { - if (name == k_colormap_names[i]) return i; +inline auto find_colormap_index(std::string_view name) -> int { + const auto names = shaders::colormap_names(); + for (std::size_t i = 0; i < names.size(); ++i) { + if (name == names[i]) { + return static_cast(i); + } } return -1; } }// namespace balsa::visualization - -#endif diff --git a/visualization/include/balsa/visualization/shaders/abstract_shader.hpp b/visualization/include/balsa/visualization/shaders/abstract_shader.hpp index 5c83f2c..dc20d51 100644 --- a/visualization/include/balsa/visualization/shaders/abstract_shader.hpp +++ b/visualization/include/balsa/visualization/shaders/abstract_shader.hpp @@ -1,9 +1,8 @@ #if !defined(BALSA_VISUALIZATION_SHADERS_ABSTRACT_SHADER_HPP) #define BALSA_VISUALIZATION_SHADERS_ABSTRACT_SHADER_HPP -#include -#include -#include +#include +#include #include // Camera is not needed by AbstractShader. @@ -11,9 +10,6 @@ namespace shaderc { class CompileOptions; } -// calls qrc which can't be used in a namespace -void balsa_visualization_shaders_initialize_resources(); - namespace balsa::visualization::shaders { class AbstractShader { @@ -23,18 +19,14 @@ class AbstractShader { Fragment }; - AbstractShader(); - std::vector compile_glsl(const std::string &glsl, ShaderType) const; - // supports qresource so can't use filesystem path - std::vector compile_glsl_from_path(const std::string &path, ShaderType) const; + AbstractShader() = default; + auto compile_glsl(std::string_view glsl, ShaderType type) const -> std::vector; virtual void add_compile_options(shaderc::CompileOptions &) const {} - virtual ~AbstractShader() {} + virtual ~AbstractShader() = default; virtual std::vector vert_spirv() const { return {}; } virtual std::vector frag_spirv() const { return {}; } - - static std::string read_path_to_string(const std::string &path); }; }// namespace balsa::visualization::shaders #endif diff --git a/visualization/include/balsa/visualization/shaders/embedded_sources.hpp b/visualization/include/balsa/visualization/shaders/embedded_sources.hpp new file mode 100644 index 0000000..55f0ab5 --- /dev/null +++ b/visualization/include/balsa/visualization/shaders/embedded_sources.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "balsa/visualization/shaders/shader_library.hpp" + +#include +#include +#include + +namespace balsa::visualization::shaders { + +auto colormap_shader_source(std::string_view name) -> std::optional; +auto colormap_names() -> std::span; + +}// namespace balsa::visualization::shaders diff --git a/visualization/include/balsa/visualization/shaders/flat.hpp b/visualization/include/balsa/visualization/shaders/flat.hpp index 35d59a1..89698b9 100644 --- a/visualization/include/balsa/visualization/shaders/flat.hpp +++ b/visualization/include/balsa/visualization/shaders/flat.hpp @@ -5,6 +5,7 @@ #include #include #include "balsa/scene_graph/embedding_traits.hpp" +#include "balsa/visualization/shaders/embedded_sources.hpp" #include "balsa/visualization/shaders/shader.hpp" namespace balsa::visualization::shaders { @@ -19,13 +20,15 @@ class FlatShader : public Shader { template std::vector FlatShader::vert_spirv() const { - const static std::string fname = ":/glsl/flat.vert"; - return AbstractShader::compile_glsl_from_path(fname, AbstractShader::ShaderType::Vertex); + const auto source = builtin_shader_library().find("flat/vertex"); + return AbstractShader::compile_glsl( + source ? std::string_view{*source} : std::string_view{}, AbstractShader::ShaderType::Vertex); } template std::vector FlatShader::frag_spirv() const { - const static std::string fname = ":/glsl/flat.frag"; - return AbstractShader::compile_glsl_from_path(fname, AbstractShader::ShaderType::Fragment); + const auto source = builtin_shader_library().find("flat/fragment"); + return AbstractShader::compile_glsl( + source ? std::string_view{*source} : std::string_view{}, AbstractShader::ShaderType::Fragment); } }// namespace balsa::visualization::shaders #endif diff --git a/visualization/include/balsa/visualization/shaders/mesh_shader.hpp b/visualization/include/balsa/visualization/shaders/mesh_shader.hpp index f417431..fef9ca5 100644 --- a/visualization/include/balsa/visualization/shaders/mesh_shader.hpp +++ b/visualization/include/balsa/visualization/shaders/mesh_shader.hpp @@ -5,6 +5,8 @@ #include #include #include "balsa/scene_graph/embedding_traits.hpp" +#include "balsa/visualization/shaders/embedded_sources.hpp" +#include "balsa/visualization/shaders/shader_library.hpp" #include "balsa/visualization/shaders/shader.hpp" #include "balsa/visualization/vulkan/mesh_render_state.hpp" @@ -24,8 +26,10 @@ class MeshShader : public Shader { public: MeshShader(const vulkan::MeshRenderState &state, vk::PrimitiveTopology topology = vk::PrimitiveTopology::eTriangleList, - bool wireframe_overlay = false) - : _state(&state), _topology(topology), _wireframe_overlay(wireframe_overlay) {} + bool wireframe_overlay = false, + const ShaderLibrary &library = builtin_shader_library()) + : _state(&state), _topology(topology), _wireframe_overlay(wireframe_overlay), + _library(library) {} void add_compile_options(shaderc::CompileOptions &opts) const override; std::vector vert_spirv() const override; @@ -35,6 +39,7 @@ class MeshShader : public Shader { const vulkan::MeshRenderState *_state; vk::PrimitiveTopology _topology; bool _wireframe_overlay; + ShaderLibrary _library; }; // ── Implementation ─────────────────────────────────────────────────── @@ -92,29 +97,33 @@ void MeshShader::add_compile_options(shaderc::CompileOptions &opts) const { template std::vector MeshShader::vert_spirv() const { - const static std::string fname = ":/glsl/mesh.vert"; - return AbstractShader::compile_glsl_from_path(fname, AbstractShader::ShaderType::Vertex); + const auto source = _library.find("mesh/vertex"); + return AbstractShader::compile_glsl( + source ? std::string_view{*source} : std::string_view{}, + AbstractShader::ShaderType::Vertex); } template std::vector MeshShader::frag_spirv() const { - // Read the base mesh fragment shader source. - std::string frag_source = AbstractShader::read_path_to_string(":/glsl/mesh.frag"); + const auto source = _library.find("mesh/fragment"); + std::string frag_source{source ? std::string_view{*source} : std::string_view{}}; if (_state->color_source == vulkan::ColorSource::ScalarField) { - // Load the colormap GLSL function from Qt resources. // The colormap shaders have NO #version directive — they are pure // function definitions exporting `vec4 colormap(float x)`. - std::string colormap_path = ":/colormap-shaders/" + _state->colormap_name + ".frag"; - std::string colormap_source = AbstractShader::read_path_to_string(colormap_path); + const auto colormap_source = colormap_shader_source(_state->colormap_name); + if (!colormap_source) { + return {}; + } // Inject the colormap source after the `#version` line. // mesh.frag starts with `#version 460 core\n` — find the end of // that line and insert the colormap definitions right after it. auto version_end = frag_source.find('\n'); if (version_end != std::string::npos) { - frag_source.insert(version_end + 1, - "\n// ── Injected colormap ──\n" + colormap_source + "\n"); + frag_source.insert( + version_end + 1, + "\n// Injected colormap\n" + std::string(*colormap_source) + "\n"); } } diff --git a/visualization/include/balsa/visualization/shaders/shader_library.hpp b/visualization/include/balsa/visualization/shaders/shader_library.hpp new file mode 100644 index 0000000..c40e4c2 --- /dev/null +++ b/visualization/include/balsa/visualization/shaders/shader_library.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace balsa::visualization::shaders { + +enum class ShaderLibraryError { + DuplicatePath, + PathNotFound, + ReadOnly, +}; + +class ShaderLibrary { + public: + using source_type = std::shared_ptr; + + ShaderLibrary(); + static auto overlay(const ShaderLibrary &parent) -> ShaderLibrary; + + // Not thread-safe. Pipelines detect revisions and rebuild on demand. + auto add_shader(std::string path, std::string source) + -> std::expected; + auto override_shader(std::string path, std::string source) + -> std::expected; + auto find(std::string_view path) const -> source_type; + auto contains(std::string_view path) const -> bool; + auto revision() const -> std::uint64_t; + auto make_read_only() -> void; + auto is_read_only() const -> bool; + + private: + struct State; + explicit ShaderLibrary(std::shared_ptr state) : _state(std::move(state)) {} + std::shared_ptr _state; +}; + +auto builtin_shader_library() -> const ShaderLibrary &; + +}// namespace balsa::visualization::shaders diff --git a/visualization/include/balsa/visualization/vulkan/ImagePipelineManager.hpp b/visualization/include/balsa/visualization/vulkan/ImagePipelineManager.hpp index bfe708c..3e46d2c 100644 --- a/visualization/include/balsa/visualization/vulkan/ImagePipelineManager.hpp +++ b/visualization/include/balsa/visualization/vulkan/ImagePipelineManager.hpp @@ -4,6 +4,7 @@ #include #include "balsa/scene_graph/types.hpp" +#include "balsa/visualization/shaders/shader_library.hpp" namespace balsa::visualization::vulkan { @@ -44,7 +45,9 @@ static_assert(sizeof(ImageParamsUBO) == 32, "ImageParamsUBO must be 32 bytes"); class ImagePipelineManager { public: - ImagePipelineManager() = default; + explicit ImagePipelineManager( + const shaders::ShaderLibrary &library = shaders::builtin_shader_library()) + : _shader_library(library) {} ~ImagePipelineManager(); // Resource address is stable because drawables retain a reference to it. @@ -56,6 +59,7 @@ class ImagePipelineManager { // Initialise with a Film reference. Must be called before any // other method. auto init(Film &film, uint32_t max_descriptor_sets = 16) -> void; + auto shader_library() const -> const shaders::ShaderLibrary & { return _shader_library; } // ── Pipeline access ───────────────────────────────────────────── @@ -105,6 +109,8 @@ class ImagePipelineManager { vk::Device _device; Film *_film = nullptr; + shaders::ShaderLibrary _shader_library; + std::uint64_t _shader_revision = 0; vk::DescriptorSetLayout _descriptor_set_layout; vk::PipelineLayout _pipeline_layout; diff --git a/visualization/include/balsa/visualization/vulkan/ImageScene.hpp b/visualization/include/balsa/visualization/vulkan/ImageScene.hpp index 758b7e4..08d3112 100644 --- a/visualization/include/balsa/visualization/vulkan/ImageScene.hpp +++ b/visualization/include/balsa/visualization/vulkan/ImageScene.hpp @@ -43,7 +43,8 @@ class VulkanImageDrawable; class ImageScene : public SceneBase { public: - ImageScene(); + explicit ImageScene( + const shaders::ShaderLibrary &library = shaders::builtin_shader_library()); ~ImageScene() override; // Non-copyable, non-movable diff --git a/visualization/include/balsa/visualization/vulkan/mesh_pipeline.hpp b/visualization/include/balsa/visualization/vulkan/mesh_pipeline.hpp index a97f42e..142e755 100644 --- a/visualization/include/balsa/visualization/vulkan/mesh_pipeline.hpp +++ b/visualization/include/balsa/visualization/vulkan/mesh_pipeline.hpp @@ -6,6 +6,7 @@ #include #include #include +#include "balsa/visualization/shaders/shader_library.hpp" #include "balsa/visualization/vulkan/mesh_render_state.hpp" @@ -92,7 +93,9 @@ MeshPipelineKey make_pipeline_key(const MeshRenderState &state, class MeshPipelineManager { public: - MeshPipelineManager() = default; + explicit MeshPipelineManager( + const shaders::ShaderLibrary &library = shaders::builtin_shader_library()) + : _shader_library(library) {} ~MeshPipelineManager(); // Non-copyable, movable @@ -105,6 +108,7 @@ class MeshPipelineManager { // other method. Safe to call again after release() to re-init // for a new device/render-pass. void init(Film &film, uint32_t max_descriptor_sets = 32); + auto shader_library() const -> const shaders::ShaderLibrary & { return _shader_library; } // ── Pipeline access ───────────────────────────────────────────── @@ -159,6 +163,8 @@ class MeshPipelineManager { vk::Device _device; Film *_film = nullptr; + shaders::ShaderLibrary _shader_library; + std::uint64_t _shader_revision = 0; vk::DescriptorSetLayout _descriptor_set_layout; vk::PipelineLayout _pipeline_layout; diff --git a/visualization/include/balsa/visualization/vulkan/mesh_scene.hpp b/visualization/include/balsa/visualization/vulkan/mesh_scene.hpp index b4edfde..06e9fad 100644 --- a/visualization/include/balsa/visualization/vulkan/mesh_scene.hpp +++ b/visualization/include/balsa/visualization/vulkan/mesh_scene.hpp @@ -41,7 +41,8 @@ class VulkanMeshDrawable; class MeshScene : public SceneBase { public: - MeshScene(); + explicit MeshScene( + const shaders::ShaderLibrary &library = shaders::builtin_shader_library()); ~MeshScene() override; // Non-copyable, non-movable (holds Vulkan resources + raw Film pointer) diff --git a/visualization/meson.build b/visualization/meson.build index c640249..317ec00 100644 --- a/visualization/meson.build +++ b/visualization/meson.build @@ -1,15 +1,40 @@ vulkan_dep = dependency('vulkan', required: false) #=========================================== -# Visualization +# Visualization #=========================================== -if not get_option('qt') - error('Qt required for shader storage, please enable to use visualization!') +shaderc_dep = dependency('shaderc') + +if get_option('imgui') and not get_option('glfw') + error('The ImGui frontend currently requires GLFW; enable -Dglfw=true or disable -Dimgui') endif -shaderc_dep = dependency('shaderc') - subdir('resources') +# Shader source files are part of the dependency contract, so a binary system +# package cannot satisfy it. Always use the pinned source subproject. +colormap_proj = subproject('colormap_shaders') +colormap_shaders_dep = colormap_proj.get_variable('colormap_shaders_dep') +colormap_shader_dir = colormap_shaders_dep.get_variable(internal: 'shader_dir') + +embed_probe = ''' +constexpr unsigned char local_shader[] = { +#embed "flat.vert" +}; +constexpr unsigned char colormap_shader[] = { +#embed "MATLAB_jet.frag" +}; +static_assert(sizeof(local_shader) > 0); +static_assert(sizeof(colormap_shader) > 0); +''' + +embed_args = [ + '--embed-dir=' + meson.current_source_dir() / 'resources' / 'glsl', + '--embed-dir=' + colormap_shader_dir, +] + +if not cc.compiles(embed_probe, args: embed_args, name: 'C++26 #embed shader resources') + error('visualization requires C++26 #embed support; disable it with -Dvisualization=false') +endif # ── ImGui ───────────────────────────────────────────────────────────────── # The WrapDB subproject compiles backend implementations (vulkan, glfw, …) @@ -21,7 +46,28 @@ shaderc_dep = dependency('shaderc') imgui_backend_sources = [] imgui_backend_inc = [] if get_option('imgui') - imgui_dep = dependency('imgui') + imgui_dep = dependency( + 'imgui', + default_options: [ + 'dx9=disabled', + 'dx10=disabled', + 'dx11=disabled', + 'dx12=disabled', + 'metal=disabled', + 'opengl=disabled', + 'sdl2_renderer=disabled', + 'sdl3_renderer=disabled', + 'sdl3_gpu=disabled', + 'vulkan=enabled', + 'webgpu=disabled', + 'glfw=enabled', + 'sdl2=disabled', + 'sdl3=disabled', + 'osx=disabled', + 'win=disabled', + 'allegro5=disabled', + ], + ) if imgui_dep.type_name() != 'internal' imgui_prefix = imgui_dep.get_variable(pkgconfig: 'prefix') imgui_bd = imgui_prefix / 'res' / 'bindings' @@ -33,8 +79,6 @@ if get_option('imgui') endif endif -colormap_shaders_dep = dependency('colormap_shaders') - # Public deps: propagated to consumers via declare_dependency(). visualization_public_deps = [vulkan_dep, shaderc_dep, dl_lib, colormap_shaders_dep] # Private deps: used only for building the library (not propagated). @@ -50,6 +94,9 @@ visualization_sources = [ 'src/vulkan/mesh_scene.cpp', 'src/vulkan/vulkan_mesh_drawable.cpp', 'src/shaders/abstract_shader.cpp', + 'src/shaders/shader_library.cpp', + 'src/shaders/embedded_sources.cpp', + 'src/shaders/embedded_colormaps.cpp', 'src/vulkan/native_film.cpp', 'src/vulkan/window.cpp', 'src/scene_graph/Object.cpp', @@ -92,8 +139,6 @@ if get_option('qt') visualization_sources += visualization_qt_sources visualization_sources += visualization_qt_mocced endif -visualization_sources += glsl_resource_sources - if get_option('glfw') glfw_dep = dependency('glfw3') visualization_public_deps += glfw_dep @@ -118,7 +163,8 @@ include_dir = [include_directories('include')] visualization_lib = library('balsaVisualization', visualization_sources, dependencies: visualization_public_deps + visualization_private_deps, - include_directories: include_dir + imgui_backend_inc) + include_directories: include_dir + imgui_backend_inc, + cpp_args: embed_args) # For consumers: propagate public deps (including imgui shared library). visualization_dep = declare_dependency(link_with: visualization_lib, dependencies: visualization_public_deps, include_directories: include_dir) @@ -127,7 +173,7 @@ meson.override_dependency('balsaVisualization', visualization_dep) if get_option('testing') subdir('tests') endif -if get_option('examples') +if get_option('examples') and get_option('glfw') and get_option('imgui') subdir('examples') endif subdir('tools') diff --git a/visualization/resources/glsl/glsl.qrc b/visualization/resources/glsl/glsl.qrc deleted file mode 100644 index 8bbcd5d..0000000 --- a/visualization/resources/glsl/glsl.qrc +++ /dev/null @@ -1,12 +0,0 @@ - - - flat.vert - flat.frag - triangle.vert - triangle.frag - mesh.vert - mesh.frag - image.vert - image.frag - - diff --git a/visualization/resources/glsl/meson.build b/visualization/resources/glsl/meson.build deleted file mode 100644 index a57359b..0000000 --- a/visualization/resources/glsl/meson.build +++ /dev/null @@ -1 +0,0 @@ -glsl_resource_sources = qt.preprocess(qresources: 'glsl.qrc') diff --git a/visualization/resources/meson.build b/visualization/resources/meson.build deleted file mode 100644 index 6af0e69..0000000 --- a/visualization/resources/meson.build +++ /dev/null @@ -1 +0,0 @@ -subdir('glsl') diff --git a/visualization/src/qt/mesh_controls_widget.cpp b/visualization/src/qt/mesh_controls_widget.cpp index 9ddb608..6474bc5 100644 --- a/visualization/src/qt/mesh_controls_widget.cpp +++ b/visualization/src/qt/mesh_controls_widget.cpp @@ -40,8 +40,7 @@ namespace vulkan = ::balsa::visualization::vulkan; namespace sg = ::balsa::scene_graph; using visualization::find_colormap_index; -using visualization::k_colormap_count; -using visualization::k_colormap_names; +using visualization::shaders::colormap_names; // ── Helper: create a labeled slider ───────────────────────────────── @@ -462,8 +461,8 @@ void MeshControlsWidget::build_color_group(QWidget *parent, auto *cmap_row = new QHBoxLayout; cmap_row->addWidget(new QLabel("Colormap:", _scalar_field_container)); _colormap_combo = new QComboBox(_scalar_field_container); - for (int i = 0; i < k_colormap_count; ++i) { - _colormap_combo->addItem(k_colormap_names[i]); + for (const auto name : colormap_names()) { + _colormap_combo->addItem(QString::fromUtf8(name.data(), static_cast(name.size()))); } cmap_row->addWidget(_colormap_combo); sf_layout->addLayout(cmap_row); @@ -1375,8 +1374,9 @@ void MeshControlsWidget::on_uniform_color_clicked() { void MeshControlsWidget::on_colormap_changed(int index) { auto *md = selected_mesh_data(); - if (!md || index < 0 || index >= k_colormap_count) return; - md->render_state().colormap_name = k_colormap_names[index]; + const auto names = colormap_names(); + if (!md || index < 0 || static_cast(index) >= names.size()) return; + md->render_state().colormap_name = names[static_cast(index)]; { QSignalBlocker block(_colormap_custom_edit); _colormap_custom_edit->setText( diff --git a/visualization/src/shaders/abstract_shader.cpp b/visualization/src/shaders/abstract_shader.cpp index 6babc0a..0c68559 100644 --- a/visualization/src/shaders/abstract_shader.cpp +++ b/visualization/src/shaders/abstract_shader.cpp @@ -1,16 +1,8 @@ #include "balsa/visualization/shaders/abstract_shader.hpp" #include "shaderc/status.h" -#include #include -#include -#include #include -#include -void balsa_visualization_shaders_initialize_resources() { - Q_INIT_RESOURCE(glsl); - Q_INIT_RESOURCE(shaders); -} namespace balsa::visualization::shaders { namespace { @@ -64,10 +56,7 @@ namespace { }// namespace -AbstractShader::AbstractShader() { - balsa_visualization_shaders_initialize_resources(); -} -std::vector AbstractShader::compile_glsl(const std::string &glsl, ShaderType type) const { +auto AbstractShader::compile_glsl(std::string_view glsl, ShaderType type) const -> std::vector { const std::string &stage_name = get_shader_type_name(type); shaderc_shader_kind kind; @@ -100,23 +89,6 @@ std::vector AbstractShader::compile_glsl(const std::string &glsl, Shad return ret; } -std::string AbstractShader::read_path_to_string(const std::string &path) { - QFile file(path.c_str()); - if (!file.open(QFile::ReadOnly | QIODevice::Text)) { - spdlog::error("Was unable to read {}", path); - return {}; - } - - QTextStream _ts(&file); - QString data = _ts.readAll(); - return data.toStdString(); -} - -std::vector AbstractShader::compile_glsl_from_path(const std::string &path, ShaderType type) const { - - return compile_glsl(read_path_to_string(path), type); -} - // namespace { // vk::ShaderModule make_shader_module(const vk::Device &device, const std::vector &spirv) { // VkShaderModuleCreateInfo createInfo{ diff --git a/visualization/src/shaders/embedded_colormaps.cpp b/visualization/src/shaders/embedded_colormaps.cpp new file mode 100644 index 0000000..5e80add --- /dev/null +++ b/visualization/src/shaders/embedded_colormaps.cpp @@ -0,0 +1,561 @@ +// Generated from the pinned colormap-shaders resource manifest. +#include "balsa/visualization/shaders/embedded_sources.hpp" + +#include + +namespace balsa::visualization::shaders { +namespace { + +constexpr unsigned char resource_IDL_16_Level[] = { +#embed "IDL_16_Level.frag" +}; +constexpr unsigned char resource_IDL_Beach[] = { +#embed "IDL_Beach.frag" +}; +constexpr unsigned char resource_IDL_Black_White_Linear[] = { +#embed "IDL_Black-White_Linear.frag" +}; +constexpr unsigned char resource_IDL_Blue_Green_Red_Yellow[] = { +#embed "IDL_Blue-Green-Red-Yellow.frag" +}; +constexpr unsigned char resource_IDL_Blue_Pastel_Red[] = { +#embed "IDL_Blue-Pastel-Red.frag" +}; +constexpr unsigned char resource_IDL_Blue_Red[] = { +#embed "IDL_Blue-Red.frag" +}; +constexpr unsigned char resource_IDL_Blue_Red_2[] = { +#embed "IDL_Blue-Red_2.frag" +}; +constexpr unsigned char resource_IDL_Blue_White_Linear[] = { +#embed "IDL_Blue-White_Linear.frag" +}; +constexpr unsigned char resource_IDL_Blue_Waves[] = { +#embed "IDL_Blue_Waves.frag" +}; +constexpr unsigned char resource_IDL_CB_Accent[] = { +#embed "IDL_CB-Accent.frag" +}; +constexpr unsigned char resource_IDL_CB_Blues[] = { +#embed "IDL_CB-Blues.frag" +}; +constexpr unsigned char resource_IDL_CB_BrBG[] = { +#embed "IDL_CB-BrBG.frag" +}; +constexpr unsigned char resource_IDL_CB_BuGn[] = { +#embed "IDL_CB-BuGn.frag" +}; +constexpr unsigned char resource_IDL_CB_BuPu[] = { +#embed "IDL_CB-BuPu.frag" +}; +constexpr unsigned char resource_IDL_CB_Dark2[] = { +#embed "IDL_CB-Dark2.frag" +}; +constexpr unsigned char resource_IDL_CB_GnBu[] = { +#embed "IDL_CB-GnBu.frag" +}; +constexpr unsigned char resource_IDL_CB_Greens[] = { +#embed "IDL_CB-Greens.frag" +}; +constexpr unsigned char resource_IDL_CB_Greys[] = { +#embed "IDL_CB-Greys.frag" +}; +constexpr unsigned char resource_IDL_CB_OrRd[] = { +#embed "IDL_CB-OrRd.frag" +}; +constexpr unsigned char resource_IDL_CB_Oranges[] = { +#embed "IDL_CB-Oranges.frag" +}; +constexpr unsigned char resource_IDL_CB_PRGn[] = { +#embed "IDL_CB-PRGn.frag" +}; +constexpr unsigned char resource_IDL_CB_Paired[] = { +#embed "IDL_CB-Paired.frag" +}; +constexpr unsigned char resource_IDL_CB_Pastel1[] = { +#embed "IDL_CB-Pastel1.frag" +}; +constexpr unsigned char resource_IDL_CB_Pastel2[] = { +#embed "IDL_CB-Pastel2.frag" +}; +constexpr unsigned char resource_IDL_CB_PiYG[] = { +#embed "IDL_CB-PiYG.frag" +}; +constexpr unsigned char resource_IDL_CB_PuBu[] = { +#embed "IDL_CB-PuBu.frag" +}; +constexpr unsigned char resource_IDL_CB_PuBuGn[] = { +#embed "IDL_CB-PuBuGn.frag" +}; +constexpr unsigned char resource_IDL_CB_PuOr[] = { +#embed "IDL_CB-PuOr.frag" +}; +constexpr unsigned char resource_IDL_CB_PuRd[] = { +#embed "IDL_CB-PuRd.frag" +}; +constexpr unsigned char resource_IDL_CB_Purples[] = { +#embed "IDL_CB-Purples.frag" +}; +constexpr unsigned char resource_IDL_CB_RdBu[] = { +#embed "IDL_CB-RdBu.frag" +}; +constexpr unsigned char resource_IDL_CB_RdGy[] = { +#embed "IDL_CB-RdGy.frag" +}; +constexpr unsigned char resource_IDL_CB_RdPu[] = { +#embed "IDL_CB-RdPu.frag" +}; +constexpr unsigned char resource_IDL_CB_RdYiBu[] = { +#embed "IDL_CB-RdYiBu.frag" +}; +constexpr unsigned char resource_IDL_CB_RdYiGn[] = { +#embed "IDL_CB-RdYiGn.frag" +}; +constexpr unsigned char resource_IDL_CB_Reds[] = { +#embed "IDL_CB-Reds.frag" +}; +constexpr unsigned char resource_IDL_CB_Set1[] = { +#embed "IDL_CB-Set1.frag" +}; +constexpr unsigned char resource_IDL_CB_Set2[] = { +#embed "IDL_CB-Set2.frag" +}; +constexpr unsigned char resource_IDL_CB_Set3[] = { +#embed "IDL_CB-Set3.frag" +}; +constexpr unsigned char resource_IDL_CB_Spectral[] = { +#embed "IDL_CB-Spectral.frag" +}; +constexpr unsigned char resource_IDL_CB_YIGn[] = { +#embed "IDL_CB-YIGn.frag" +}; +constexpr unsigned char resource_IDL_CB_YIGnBu[] = { +#embed "IDL_CB-YIGnBu.frag" +}; +constexpr unsigned char resource_IDL_CB_YIOrBr[] = { +#embed "IDL_CB-YIOrBr.frag" +}; +constexpr unsigned char resource_IDL_Eos_A[] = { +#embed "IDL_Eos_A.frag" +}; +constexpr unsigned char resource_IDL_Eos_B[] = { +#embed "IDL_Eos_B.frag" +}; +constexpr unsigned char resource_IDL_Green_Pink[] = { +#embed "IDL_Green-Pink.frag" +}; +constexpr unsigned char resource_IDL_Green_Red_Blue_White[] = { +#embed "IDL_Green-Red-Blue-White.frag" +}; +constexpr unsigned char resource_IDL_Green_White_Exponential[] = { +#embed "IDL_Green-White_Exponential.frag" +}; +constexpr unsigned char resource_IDL_Green_White_Linear[] = { +#embed "IDL_Green-White_Linear.frag" +}; +constexpr unsigned char resource_IDL_Hardcandy[] = { +#embed "IDL_Hardcandy.frag" +}; +constexpr unsigned char resource_IDL_Haze[] = { +#embed "IDL_Haze.frag" +}; +constexpr unsigned char resource_IDL_Hue_Sat_Lightness_1[] = { +#embed "IDL_Hue_Sat_Lightness_1.frag" +}; +constexpr unsigned char resource_IDL_Hue_Sat_Lightness_2[] = { +#embed "IDL_Hue_Sat_Lightness_2.frag" +}; +constexpr unsigned char resource_IDL_Hue_Sat_Value_1[] = { +#embed "IDL_Hue_Sat_Value_1.frag" +}; +constexpr unsigned char resource_IDL_Hue_Sat_Value_2[] = { +#embed "IDL_Hue_Sat_Value_2.frag" +}; +constexpr unsigned char resource_IDL_Mac_Style[] = { +#embed "IDL_Mac_Style.frag" +}; +constexpr unsigned char resource_IDL_Nature[] = { +#embed "IDL_Nature.frag" +}; +constexpr unsigned char resource_IDL_Ocean[] = { +#embed "IDL_Ocean.frag" +}; +constexpr unsigned char resource_IDL_Pastels[] = { +#embed "IDL_Pastels.frag" +}; +constexpr unsigned char resource_IDL_Peppermint[] = { +#embed "IDL_Peppermint.frag" +}; +constexpr unsigned char resource_IDL_Plasma[] = { +#embed "IDL_Plasma.frag" +}; +constexpr unsigned char resource_IDL_Prism[] = { +#embed "IDL_Prism.frag" +}; +constexpr unsigned char resource_IDL_Purple_Red_Stripes[] = { +#embed "IDL_Purple-Red+Stripes.frag" +}; +constexpr unsigned char resource_IDL_Rainbow_Black[] = { +#embed "IDL_Rainbow+Black.frag" +}; +constexpr unsigned char resource_IDL_Rainbow_White[] = { +#embed "IDL_Rainbow+White.frag" +}; +constexpr unsigned char resource_IDL_Rainbow[] = { +#embed "IDL_Rainbow.frag" +}; +constexpr unsigned char resource_IDL_Rainbow_18[] = { +#embed "IDL_Rainbow_18.frag" +}; +constexpr unsigned char resource_IDL_Rainbow_2[] = { +#embed "IDL_Rainbow_2.frag" +}; +constexpr unsigned char resource_IDL_Red_Purple[] = { +#embed "IDL_Red-Purple.frag" +}; +constexpr unsigned char resource_IDL_Red_Temperature[] = { +#embed "IDL_Red_Temperature.frag" +}; +constexpr unsigned char resource_IDL_Standard_Gamma_II[] = { +#embed "IDL_Standard_Gamma-II.frag" +}; +constexpr unsigned char resource_IDL_Steps[] = { +#embed "IDL_Steps.frag" +}; +constexpr unsigned char resource_IDL_Stern_Special[] = { +#embed "IDL_Stern_Special.frag" +}; +constexpr unsigned char resource_IDL_Volcano[] = { +#embed "IDL_Volcano.frag" +}; +constexpr unsigned char resource_IDL_Waves[] = { +#embed "IDL_Waves.frag" +}; +constexpr unsigned char resource_MATLAB_autumn[] = { +#embed "MATLAB_autumn.frag" +}; +constexpr unsigned char resource_MATLAB_bone[] = { +#embed "MATLAB_bone.frag" +}; +constexpr unsigned char resource_MATLAB_cool[] = { +#embed "MATLAB_cool.frag" +}; +constexpr unsigned char resource_MATLAB_copper[] = { +#embed "MATLAB_copper.frag" +}; +constexpr unsigned char resource_MATLAB_hot[] = { +#embed "MATLAB_hot.frag" +}; +constexpr unsigned char resource_MATLAB_hsv[] = { +#embed "MATLAB_hsv.frag" +}; +constexpr unsigned char resource_MATLAB_jet[] = { +#embed "MATLAB_jet.frag" +}; +constexpr unsigned char resource_MATLAB_parula[] = { +#embed "MATLAB_parula.frag" +}; +constexpr unsigned char resource_MATLAB_pink[] = { +#embed "MATLAB_pink.frag" +}; +constexpr unsigned char resource_MATLAB_spring[] = { +#embed "MATLAB_spring.frag" +}; +constexpr unsigned char resource_MATLAB_summer[] = { +#embed "MATLAB_summer.frag" +}; +constexpr unsigned char resource_MATLAB_winter[] = { +#embed "MATLAB_winter.frag" +}; +constexpr unsigned char resource_kbinani_altitude[] = { +#embed "kbinani_altitude.frag" +}; +constexpr unsigned char resource_transform_apricot[] = { +#embed "transform_apricot.frag" +}; +constexpr unsigned char resource_transform_carnation[] = { +#embed "transform_carnation.frag" +}; +constexpr unsigned char resource_transform_ether[] = { +#embed "transform_ether.frag" +}; +constexpr unsigned char resource_transform_grayscale_banded[] = { +#embed "transform_grayscale_banded.frag" +}; +constexpr unsigned char resource_transform_hot_metal[] = { +#embed "transform_hot_metal.frag" +}; +constexpr unsigned char resource_transform_lava_waves[] = { +#embed "transform_lava_waves.frag" +}; +constexpr unsigned char resource_transform_malachite[] = { +#embed "transform_malachite.frag" +}; +constexpr unsigned char resource_transform_morning_glory[] = { +#embed "transform_morning_glory.frag" +}; +constexpr unsigned char resource_transform_peanut_butter_and_jerry[] = { +#embed "transform_peanut_butter_and_jerry.frag" +}; +constexpr unsigned char resource_transform_purple_haze[] = { +#embed "transform_purple_haze.frag" +}; +constexpr unsigned char resource_transform_rainbow[] = { +#embed "transform_rainbow.frag" +}; +constexpr unsigned char resource_transform_rose[] = { +#embed "transform_rose.frag" +}; +constexpr unsigned char resource_transform_saturn[] = { +#embed "transform_saturn.frag" +}; +constexpr unsigned char resource_transform_seismic[] = { +#embed "transform_seismic.frag" +}; +constexpr unsigned char resource_transform_space[] = { +#embed "transform_space.frag" +}; +constexpr unsigned char resource_transform_supernova[] = { +#embed "transform_supernova.frag" +}; + +template +auto as_string_view(const unsigned char (&data)[N]) -> std::string_view { + return {reinterpret_cast(data), N}; +} + +struct NamedSource { + std::string_view name; + std::string_view source; +}; + +const std::array colormaps{{ + {"IDL_16_Level", as_string_view(resource_IDL_16_Level)}, + {"IDL_Beach", as_string_view(resource_IDL_Beach)}, + {"IDL_Black-White_Linear", as_string_view(resource_IDL_Black_White_Linear)}, + {"IDL_Blue-Green-Red-Yellow", as_string_view(resource_IDL_Blue_Green_Red_Yellow)}, + {"IDL_Blue-Pastel-Red", as_string_view(resource_IDL_Blue_Pastel_Red)}, + {"IDL_Blue-Red", as_string_view(resource_IDL_Blue_Red)}, + {"IDL_Blue-Red_2", as_string_view(resource_IDL_Blue_Red_2)}, + {"IDL_Blue-White_Linear", as_string_view(resource_IDL_Blue_White_Linear)}, + {"IDL_Blue_Waves", as_string_view(resource_IDL_Blue_Waves)}, + {"IDL_CB-Accent", as_string_view(resource_IDL_CB_Accent)}, + {"IDL_CB-Blues", as_string_view(resource_IDL_CB_Blues)}, + {"IDL_CB-BrBG", as_string_view(resource_IDL_CB_BrBG)}, + {"IDL_CB-BuGn", as_string_view(resource_IDL_CB_BuGn)}, + {"IDL_CB-BuPu", as_string_view(resource_IDL_CB_BuPu)}, + {"IDL_CB-Dark2", as_string_view(resource_IDL_CB_Dark2)}, + {"IDL_CB-GnBu", as_string_view(resource_IDL_CB_GnBu)}, + {"IDL_CB-Greens", as_string_view(resource_IDL_CB_Greens)}, + {"IDL_CB-Greys", as_string_view(resource_IDL_CB_Greys)}, + {"IDL_CB-OrRd", as_string_view(resource_IDL_CB_OrRd)}, + {"IDL_CB-Oranges", as_string_view(resource_IDL_CB_Oranges)}, + {"IDL_CB-PRGn", as_string_view(resource_IDL_CB_PRGn)}, + {"IDL_CB-Paired", as_string_view(resource_IDL_CB_Paired)}, + {"IDL_CB-Pastel1", as_string_view(resource_IDL_CB_Pastel1)}, + {"IDL_CB-Pastel2", as_string_view(resource_IDL_CB_Pastel2)}, + {"IDL_CB-PiYG", as_string_view(resource_IDL_CB_PiYG)}, + {"IDL_CB-PuBu", as_string_view(resource_IDL_CB_PuBu)}, + {"IDL_CB-PuBuGn", as_string_view(resource_IDL_CB_PuBuGn)}, + {"IDL_CB-PuOr", as_string_view(resource_IDL_CB_PuOr)}, + {"IDL_CB-PuRd", as_string_view(resource_IDL_CB_PuRd)}, + {"IDL_CB-Purples", as_string_view(resource_IDL_CB_Purples)}, + {"IDL_CB-RdBu", as_string_view(resource_IDL_CB_RdBu)}, + {"IDL_CB-RdGy", as_string_view(resource_IDL_CB_RdGy)}, + {"IDL_CB-RdPu", as_string_view(resource_IDL_CB_RdPu)}, + {"IDL_CB-RdYiBu", as_string_view(resource_IDL_CB_RdYiBu)}, + {"IDL_CB-RdYiGn", as_string_view(resource_IDL_CB_RdYiGn)}, + {"IDL_CB-Reds", as_string_view(resource_IDL_CB_Reds)}, + {"IDL_CB-Set1", as_string_view(resource_IDL_CB_Set1)}, + {"IDL_CB-Set2", as_string_view(resource_IDL_CB_Set2)}, + {"IDL_CB-Set3", as_string_view(resource_IDL_CB_Set3)}, + {"IDL_CB-Spectral", as_string_view(resource_IDL_CB_Spectral)}, + {"IDL_CB-YIGn", as_string_view(resource_IDL_CB_YIGn)}, + {"IDL_CB-YIGnBu", as_string_view(resource_IDL_CB_YIGnBu)}, + {"IDL_CB-YIOrBr", as_string_view(resource_IDL_CB_YIOrBr)}, + {"IDL_Eos_A", as_string_view(resource_IDL_Eos_A)}, + {"IDL_Eos_B", as_string_view(resource_IDL_Eos_B)}, + {"IDL_Green-Pink", as_string_view(resource_IDL_Green_Pink)}, + {"IDL_Green-Red-Blue-White", as_string_view(resource_IDL_Green_Red_Blue_White)}, + {"IDL_Green-White_Exponential", as_string_view(resource_IDL_Green_White_Exponential)}, + {"IDL_Green-White_Linear", as_string_view(resource_IDL_Green_White_Linear)}, + {"IDL_Hardcandy", as_string_view(resource_IDL_Hardcandy)}, + {"IDL_Haze", as_string_view(resource_IDL_Haze)}, + {"IDL_Hue_Sat_Lightness_1", as_string_view(resource_IDL_Hue_Sat_Lightness_1)}, + {"IDL_Hue_Sat_Lightness_2", as_string_view(resource_IDL_Hue_Sat_Lightness_2)}, + {"IDL_Hue_Sat_Value_1", as_string_view(resource_IDL_Hue_Sat_Value_1)}, + {"IDL_Hue_Sat_Value_2", as_string_view(resource_IDL_Hue_Sat_Value_2)}, + {"IDL_Mac_Style", as_string_view(resource_IDL_Mac_Style)}, + {"IDL_Nature", as_string_view(resource_IDL_Nature)}, + {"IDL_Ocean", as_string_view(resource_IDL_Ocean)}, + {"IDL_Pastels", as_string_view(resource_IDL_Pastels)}, + {"IDL_Peppermint", as_string_view(resource_IDL_Peppermint)}, + {"IDL_Plasma", as_string_view(resource_IDL_Plasma)}, + {"IDL_Prism", as_string_view(resource_IDL_Prism)}, + {"IDL_Purple-Red+Stripes", as_string_view(resource_IDL_Purple_Red_Stripes)}, + {"IDL_Rainbow+Black", as_string_view(resource_IDL_Rainbow_Black)}, + {"IDL_Rainbow+White", as_string_view(resource_IDL_Rainbow_White)}, + {"IDL_Rainbow", as_string_view(resource_IDL_Rainbow)}, + {"IDL_Rainbow_18", as_string_view(resource_IDL_Rainbow_18)}, + {"IDL_Rainbow_2", as_string_view(resource_IDL_Rainbow_2)}, + {"IDL_Red-Purple", as_string_view(resource_IDL_Red_Purple)}, + {"IDL_Red_Temperature", as_string_view(resource_IDL_Red_Temperature)}, + {"IDL_Standard_Gamma-II", as_string_view(resource_IDL_Standard_Gamma_II)}, + {"IDL_Steps", as_string_view(resource_IDL_Steps)}, + {"IDL_Stern_Special", as_string_view(resource_IDL_Stern_Special)}, + {"IDL_Volcano", as_string_view(resource_IDL_Volcano)}, + {"IDL_Waves", as_string_view(resource_IDL_Waves)}, + {"MATLAB_autumn", as_string_view(resource_MATLAB_autumn)}, + {"MATLAB_bone", as_string_view(resource_MATLAB_bone)}, + {"MATLAB_cool", as_string_view(resource_MATLAB_cool)}, + {"MATLAB_copper", as_string_view(resource_MATLAB_copper)}, + {"MATLAB_hot", as_string_view(resource_MATLAB_hot)}, + {"MATLAB_hsv", as_string_view(resource_MATLAB_hsv)}, + {"MATLAB_jet", as_string_view(resource_MATLAB_jet)}, + {"MATLAB_parula", as_string_view(resource_MATLAB_parula)}, + {"MATLAB_pink", as_string_view(resource_MATLAB_pink)}, + {"MATLAB_spring", as_string_view(resource_MATLAB_spring)}, + {"MATLAB_summer", as_string_view(resource_MATLAB_summer)}, + {"MATLAB_winter", as_string_view(resource_MATLAB_winter)}, + {"kbinani_altitude", as_string_view(resource_kbinani_altitude)}, + {"transform_apricot", as_string_view(resource_transform_apricot)}, + {"transform_carnation", as_string_view(resource_transform_carnation)}, + {"transform_ether", as_string_view(resource_transform_ether)}, + {"transform_grayscale_banded", as_string_view(resource_transform_grayscale_banded)}, + {"transform_hot_metal", as_string_view(resource_transform_hot_metal)}, + {"transform_lava_waves", as_string_view(resource_transform_lava_waves)}, + {"transform_malachite", as_string_view(resource_transform_malachite)}, + {"transform_morning_glory", as_string_view(resource_transform_morning_glory)}, + {"transform_peanut_butter_and_jerry", as_string_view(resource_transform_peanut_butter_and_jerry)}, + {"transform_purple_haze", as_string_view(resource_transform_purple_haze)}, + {"transform_rainbow", as_string_view(resource_transform_rainbow)}, + {"transform_rose", as_string_view(resource_transform_rose)}, + {"transform_saturn", as_string_view(resource_transform_saturn)}, + {"transform_seismic", as_string_view(resource_transform_seismic)}, + {"transform_space", as_string_view(resource_transform_space)}, + {"transform_supernova", as_string_view(resource_transform_supernova)}, +}}; + +const std::array names{{ + "IDL_16_Level", + "IDL_Beach", + "IDL_Black-White_Linear", + "IDL_Blue-Green-Red-Yellow", + "IDL_Blue-Pastel-Red", + "IDL_Blue-Red", + "IDL_Blue-Red_2", + "IDL_Blue-White_Linear", + "IDL_Blue_Waves", + "IDL_CB-Accent", + "IDL_CB-Blues", + "IDL_CB-BrBG", + "IDL_CB-BuGn", + "IDL_CB-BuPu", + "IDL_CB-Dark2", + "IDL_CB-GnBu", + "IDL_CB-Greens", + "IDL_CB-Greys", + "IDL_CB-OrRd", + "IDL_CB-Oranges", + "IDL_CB-PRGn", + "IDL_CB-Paired", + "IDL_CB-Pastel1", + "IDL_CB-Pastel2", + "IDL_CB-PiYG", + "IDL_CB-PuBu", + "IDL_CB-PuBuGn", + "IDL_CB-PuOr", + "IDL_CB-PuRd", + "IDL_CB-Purples", + "IDL_CB-RdBu", + "IDL_CB-RdGy", + "IDL_CB-RdPu", + "IDL_CB-RdYiBu", + "IDL_CB-RdYiGn", + "IDL_CB-Reds", + "IDL_CB-Set1", + "IDL_CB-Set2", + "IDL_CB-Set3", + "IDL_CB-Spectral", + "IDL_CB-YIGn", + "IDL_CB-YIGnBu", + "IDL_CB-YIOrBr", + "IDL_Eos_A", + "IDL_Eos_B", + "IDL_Green-Pink", + "IDL_Green-Red-Blue-White", + "IDL_Green-White_Exponential", + "IDL_Green-White_Linear", + "IDL_Hardcandy", + "IDL_Haze", + "IDL_Hue_Sat_Lightness_1", + "IDL_Hue_Sat_Lightness_2", + "IDL_Hue_Sat_Value_1", + "IDL_Hue_Sat_Value_2", + "IDL_Mac_Style", + "IDL_Nature", + "IDL_Ocean", + "IDL_Pastels", + "IDL_Peppermint", + "IDL_Plasma", + "IDL_Prism", + "IDL_Purple-Red+Stripes", + "IDL_Rainbow+Black", + "IDL_Rainbow+White", + "IDL_Rainbow", + "IDL_Rainbow_18", + "IDL_Rainbow_2", + "IDL_Red-Purple", + "IDL_Red_Temperature", + "IDL_Standard_Gamma-II", + "IDL_Steps", + "IDL_Stern_Special", + "IDL_Volcano", + "IDL_Waves", + "MATLAB_autumn", + "MATLAB_bone", + "MATLAB_cool", + "MATLAB_copper", + "MATLAB_hot", + "MATLAB_hsv", + "MATLAB_jet", + "MATLAB_parula", + "MATLAB_pink", + "MATLAB_spring", + "MATLAB_summer", + "MATLAB_winter", + "kbinani_altitude", + "transform_apricot", + "transform_carnation", + "transform_ether", + "transform_grayscale_banded", + "transform_hot_metal", + "transform_lava_waves", + "transform_malachite", + "transform_morning_glory", + "transform_peanut_butter_and_jerry", + "transform_purple_haze", + "transform_rainbow", + "transform_rose", + "transform_saturn", + "transform_seismic", + "transform_space", + "transform_supernova", +}}; + +}// namespace + +auto colormap_shader_source(std::string_view name) -> std::optional { + for (const auto &entry : colormaps) { + if (entry.name == name) { + return entry.source; + } + } + return std::nullopt; +} + +auto colormap_names() -> std::span { + return names; +} + +}// namespace balsa::visualization::shaders diff --git a/visualization/src/shaders/embedded_sources.cpp b/visualization/src/shaders/embedded_sources.cpp new file mode 100644 index 0000000..e1fc138 --- /dev/null +++ b/visualization/src/shaders/embedded_sources.cpp @@ -0,0 +1,58 @@ +#include "balsa/visualization/shaders/embedded_sources.hpp" + +namespace balsa::visualization::shaders { +namespace { + +constexpr unsigned char flat_vertex[] = { +#embed "flat.vert" +}; +constexpr unsigned char flat_fragment[] = { +#embed "flat.frag" +}; +constexpr unsigned char triangle_vertex[] = { +#embed "triangle.vert" +}; +constexpr unsigned char triangle_fragment[] = { +#embed "triangle.frag" +}; +constexpr unsigned char mesh_vertex[] = { +#embed "mesh.vert" +}; +constexpr unsigned char mesh_fragment[] = { +#embed "mesh.frag" +}; +constexpr unsigned char image_vertex[] = { +#embed "image.vert" +}; +constexpr unsigned char image_fragment[] = { +#embed "image.frag" +}; + +template +auto as_string_view(const unsigned char (&data)[N]) -> std::string_view { + return {reinterpret_cast(data), N}; +} + +}// namespace + +auto builtin_shader_library() -> const ShaderLibrary & { + static ShaderLibrary library; + static const bool initialized = [&] { + (void)library.add_shader("flat/vertex", std::string{as_string_view(flat_vertex)}); + (void)library.add_shader("flat/fragment", std::string{as_string_view(flat_fragment)}); + (void)library.add_shader( + "examples/triangle/vertex", std::string{as_string_view(triangle_vertex)}); + (void)library.add_shader( + "examples/triangle/fragment", std::string{as_string_view(triangle_fragment)}); + (void)library.add_shader("mesh/vertex", std::string{as_string_view(mesh_vertex)}); + (void)library.add_shader("mesh/fragment", std::string{as_string_view(mesh_fragment)}); + (void)library.add_shader("image/vertex", std::string{as_string_view(image_vertex)}); + (void)library.add_shader("image/fragment", std::string{as_string_view(image_fragment)}); + library.make_read_only(); + return true; + }(); + (void)initialized; + return library; +} + +}// namespace balsa::visualization::shaders diff --git a/visualization/src/shaders/shader_library.cpp b/visualization/src/shaders/shader_library.cpp new file mode 100644 index 0000000..2ae091d --- /dev/null +++ b/visualization/src/shaders/shader_library.cpp @@ -0,0 +1,81 @@ +#include "balsa/visualization/shaders/shader_library.hpp" + +namespace balsa::visualization::shaders { + +struct ShaderLibrary::State { + std::shared_ptr parent; + std::map> sources; + std::uint64_t revision = 0; + bool read_only = false; +}; + +ShaderLibrary::ShaderLibrary() : _state(std::make_shared()) {} + +auto ShaderLibrary::overlay(const ShaderLibrary &parent) -> ShaderLibrary { + auto state = std::make_shared(); + state->parent = parent._state; + return ShaderLibrary{std::move(state)}; +} + +auto ShaderLibrary::add_shader(std::string path, std::string source) + -> std::expected { + if (_state->read_only) { + return std::unexpected(ShaderLibraryError::ReadOnly); + } + if (contains(path)) { + return std::unexpected(ShaderLibraryError::DuplicatePath); + } + const auto [_, inserted] = _state->sources.emplace( + std::move(path), std::make_shared(std::move(source))); + if (!inserted) { + return std::unexpected(ShaderLibraryError::DuplicatePath); + } + ++_state->revision; + return {}; +} + +auto ShaderLibrary::override_shader(std::string path, std::string source) + -> std::expected { + if (_state->read_only) { + return std::unexpected(ShaderLibraryError::ReadOnly); + } + if (!contains(path)) { + return std::unexpected(ShaderLibraryError::PathNotFound); + } + _state->sources.insert_or_assign( + std::move(path), std::make_shared(std::move(source))); + ++_state->revision; + return {}; +} + +auto ShaderLibrary::find(std::string_view path) const -> source_type { + for (auto state = _state; state; state = state->parent) { + const auto it = state->sources.find(path); + if (it != state->sources.end()) { + return it->second; + } + } + return nullptr; +} + +auto ShaderLibrary::contains(std::string_view path) const -> bool { + return static_cast(find(path)); +} + +auto ShaderLibrary::revision() const -> std::uint64_t { + std::uint64_t revision = _state->revision; + for (auto parent = _state->parent; parent; parent = parent->parent) { + revision += parent->revision; + } + return revision; +} + +auto ShaderLibrary::make_read_only() -> void { + _state->read_only = true; +} + +auto ShaderLibrary::is_read_only() const -> bool { + return _state->read_only; +} + +}// namespace balsa::visualization::shaders diff --git a/visualization/src/vulkan/ImagePipelineManager.cpp b/visualization/src/vulkan/ImagePipelineManager.cpp index bc34e6f..ef1490c 100644 --- a/visualization/src/vulkan/ImagePipelineManager.cpp +++ b/visualization/src/vulkan/ImagePipelineManager.cpp @@ -1,4 +1,5 @@ #include "balsa/visualization/vulkan/ImagePipelineManager.hpp" +#include "balsa/visualization/shaders/embedded_sources.hpp" #include "balsa/visualization/shaders/abstract_shader.hpp" #include "balsa/visualization/vulkan/film.hpp" @@ -21,6 +22,7 @@ auto ImagePipelineManager::init(Film &film, uint32_t max_descriptor_sets) -> voi create_descriptor_pool(max_descriptor_sets); _initialized = true; + _shader_revision = _shader_library.revision(); spdlog::info("ImagePipelineManager: initialized"); } @@ -196,6 +198,11 @@ auto ImagePipelineManager::get_or_create() -> vk::Pipeline { } auto &film = *_film; + if (_shader_revision != _shader_library.revision()) { + invalidate_pipeline(); + _shader_revision = _shader_library.revision(); + } + // Check if the cached pipeline is still valid for this render pass. const vk::RenderPass rp = film.default_render_pass(); uint32_t msaa = static_cast(film.sample_count()); @@ -226,12 +233,15 @@ auto ImagePipelineManager::get_or_create() -> vk::Pipeline { auto ImagePipelineManager::create_pipeline() -> vk::Pipeline { spdlog::info("ImagePipelineManager: creating pipeline"); - // Compile shaders from Qt resources. shaders::AbstractShader shader_compiler; - auto vert_spv = shader_compiler.compile_glsl_from_path( - ":/glsl/image.vert", shaders::AbstractShader::ShaderType::Vertex); - auto frag_spv = shader_compiler.compile_glsl_from_path( - ":/glsl/image.frag", shaders::AbstractShader::ShaderType::Fragment); + const auto vertex_source = _shader_library.find("image/vertex"); + const auto fragment_source = _shader_library.find("image/fragment"); + auto vert_spv = shader_compiler.compile_glsl( + vertex_source ? std::string_view{*vertex_source} : std::string_view{}, + shaders::AbstractShader::ShaderType::Vertex); + auto frag_spv = shader_compiler.compile_glsl( + fragment_source ? std::string_view{*fragment_source} : std::string_view{}, + shaders::AbstractShader::ShaderType::Fragment); if (vert_spv.empty() || frag_spv.empty()) { spdlog::error( diff --git a/visualization/src/vulkan/ImageScene.cpp b/visualization/src/vulkan/ImageScene.cpp index fc0a75e..5eb08fe 100644 --- a/visualization/src/vulkan/ImageScene.cpp +++ b/visualization/src/vulkan/ImageScene.cpp @@ -11,7 +11,8 @@ namespace balsa::visualization::vulkan { // ── Constructor / Destructor ──────────────────────────────────────── -ImageScene::ImageScene() : _scene_root("Root") { +ImageScene::ImageScene(const shaders::ShaderLibrary &library) + : _scene_root("Root"), _pipeline_manager(library) { // Create a camera Object as a child of the root. auto &cam_obj = _scene_root.add_child("Camera"); cam_obj.permanent = true; diff --git a/visualization/src/vulkan/imgui/mesh_controls_panel.cpp b/visualization/src/vulkan/imgui/mesh_controls_panel.cpp index cdc91d8..cb2ad50 100644 --- a/visualization/src/vulkan/imgui/mesh_controls_panel.cpp +++ b/visualization/src/vulkan/imgui/mesh_controls_panel.cpp @@ -17,8 +17,7 @@ namespace balsa::visualization::vulkan::imgui { using visualization::find_colormap_index; -using visualization::k_colormap_count; -using visualization::k_colormap_names; +using visualization::shaders::colormap_names; // ── Enum combo helpers ─────────────────────────────────────────────── @@ -230,15 +229,16 @@ bool draw_render_state_controls(MeshRenderState &state, bool has_normals) { if (state.color_source == ColorSource::ScalarField) { // Colormap combo box + const auto names = colormap_names(); int cmap_idx = find_colormap_index(state.colormap_name); if (ImGui::BeginCombo("Colormap", cmap_idx >= 0 - ? k_colormap_names[cmap_idx] + ? names[static_cast(cmap_idx)].data() : state.colormap_name.c_str())) { - for (int i = 0; i < k_colormap_count; ++i) { - bool selected = (i == cmap_idx); - if (ImGui::Selectable(k_colormap_names[i], selected)) { - state.colormap_name = k_colormap_names[i]; + for (std::size_t i = 0; i < names.size(); ++i) { + bool selected = (static_cast(i) == cmap_idx); + if (ImGui::Selectable(names[i].data(), selected)) { + state.colormap_name = names[i]; changed = true; } if (selected) ImGui::SetItemDefaultFocus(); diff --git a/visualization/src/vulkan/mesh_pipeline.cpp b/visualization/src/vulkan/mesh_pipeline.cpp index b66775c..20450c1 100644 --- a/visualization/src/vulkan/mesh_pipeline.cpp +++ b/visualization/src/vulkan/mesh_pipeline.cpp @@ -93,7 +93,8 @@ MeshPipelineKey make_pipeline_key(const MeshRenderState &state, MeshPipelineManager::~MeshPipelineManager() { release(); } MeshPipelineManager::MeshPipelineManager(MeshPipelineManager &&o) noexcept - : _device(o._device), _film(o._film), + : _device(o._device), _film(o._film), _shader_library(o._shader_library), + _shader_revision(o._shader_revision), _descriptor_set_layout(o._descriptor_set_layout), _pipeline_layout(o._pipeline_layout), _descriptor_pool(o._descriptor_pool), _cache(std::move(o._cache)), _initialized(o._initialized) { @@ -111,6 +112,8 @@ MeshPipelineManager & release(); _device = o._device; _film = o._film; + _shader_library = o._shader_library; + _shader_revision = o._shader_revision; _descriptor_set_layout = o._descriptor_set_layout; _pipeline_layout = o._pipeline_layout; _descriptor_pool = o._descriptor_pool; @@ -136,6 +139,7 @@ void MeshPipelineManager::init(Film &film, uint32_t max_descriptor_sets) { create_descriptor_pool(max_descriptor_sets); _initialized = true; + _shader_revision = _shader_library.revision(); spdlog::info("MeshPipelineManager: initialized"); } @@ -298,6 +302,10 @@ vk::Pipeline MeshPipelineManager::get_or_create(const MeshRenderState &state, if (!_initialized) { throw std::runtime_error("MeshPipelineManager: not initialized"); } + if (_shader_revision != _shader_library.revision()) { + invalidate_pipelines(); + _shader_revision = _shader_library.revision(); + } auto key = make_pipeline_key(state, topology, buffers, film, wireframe_overlay); auto it = _cache.find(key); @@ -327,7 +335,7 @@ vk::Pipeline MeshPipelineManager::create_pipeline(const MeshPipelineKey &key) { using ET3F = scene_graph::embedding_traits3F; shaders::MeshShader shader( - temp_state, key.topology, key.wireframe_overlay); + temp_state, key.topology, key.wireframe_overlay, _shader_library); auto vert_spv = shader.vert_spirv(); auto frag_spv = shader.frag_spirv(); diff --git a/visualization/src/vulkan/mesh_scene.cpp b/visualization/src/vulkan/mesh_scene.cpp index 59758ba..6120ba8 100644 --- a/visualization/src/vulkan/mesh_scene.cpp +++ b/visualization/src/vulkan/mesh_scene.cpp @@ -12,8 +12,8 @@ namespace balsa::visualization::vulkan { // ── Constructor / Destructor ───────────────────────────────────────── -MeshScene::MeshScene() - : _scene_root("Root") { +MeshScene::MeshScene(const shaders::ShaderLibrary &library) + : _scene_root("Root"), _pipeline_manager(library) { // Create a camera Object as a child of the root. auto &cam_obj = _scene_root.add_child("Camera"); cam_obj.permanent = true;// internal fixture — hidden from UI, cannot be deleted diff --git a/visualization/tests/meson.build b/visualization/tests/meson.build index ea24195..666178c 100644 --- a/visualization/tests/meson.build +++ b/visualization/tests/meson.build @@ -1,6 +1,13 @@ -catch2_with_main_dep = dependency('catch2-with-main', allow_fallback: true) +catch2_with_main_dep = dependency('catch2-with-main', allow_fallback: true, default_options: ['tests=false']) test_scene_graph = executable('test_scene_graph', 'test_scene_graph.cpp', dependencies: [visualization_dep,catch2_with_main_dep]) test('scene_graph test', test_scene_graph) + +test_embedded_shaders = executable( + 'test_embedded_shaders', + 'test_embedded_shaders.cpp', + dependencies: [visualization_dep, catch2_with_main_dep], +) +test('embedded shader test', test_embedded_shaders) diff --git a/visualization/tests/test_embedded_shaders.cpp b/visualization/tests/test_embedded_shaders.cpp new file mode 100644 index 0000000..53624fd --- /dev/null +++ b/visualization/tests/test_embedded_shaders.cpp @@ -0,0 +1,137 @@ +#include + +#include "balsa/visualization/colormap_list.hpp" +#include "balsa/visualization/shaders/abstract_shader.hpp" +#include "balsa/visualization/shaders/embedded_sources.hpp" +#include "balsa/visualization/shaders/shader_library.hpp" + +#include +#include + +namespace shaders = balsa::visualization::shaders; + +TEST_CASE("shader_library_supports_extension", "[visualization][shaders]") { + shaders::ShaderLibrary library; + + REQUIRE(library.add_shader("user/vertex", "vertex source").has_value()); + CHECK(library.contains("user/vertex")); + REQUIRE(library.find("user/vertex")); + CHECK(*library.find("user/vertex") == "vertex source"); + + const auto duplicate = library.add_shader("user/vertex", "duplicate"); + REQUIRE_FALSE(duplicate.has_value()); + CHECK(duplicate.error() == shaders::ShaderLibraryError::DuplicatePath); + CHECK(*library.find("user/vertex") == "vertex source"); +} + +TEST_CASE("shader_library_overlays_builtins", "[visualization][shaders]") { + auto library = shaders::ShaderLibrary::overlay(shaders::builtin_shader_library()); + const auto initial_revision = library.revision(); + + REQUIRE(library.find("mesh/vertex")); + REQUIRE(library.add_shader("user/fragment", "user source").has_value()); + CHECK(library.revision() > initial_revision); + + const auto duplicate = library.add_shader("mesh/vertex", "duplicate"); + REQUIRE_FALSE(duplicate.has_value()); + CHECK(duplicate.error() == shaders::ShaderLibraryError::DuplicatePath); + + const auto old_source = library.find("mesh/vertex"); + REQUIRE(library.override_shader("mesh/vertex", "override source").has_value()); + CHECK(*library.find("mesh/vertex") == "override source"); + CHECK(old_source->starts_with("#version")); + + const auto missing = library.override_shader("missing", "source"); + REQUIRE_FALSE(missing.has_value()); + CHECK(missing.error() == shaders::ShaderLibraryError::PathNotFound); +} + +TEST_CASE("shader_library_shares_revisions_safely", "[visualization][shaders]") { + shaders::ShaderLibrary parent; + auto overlay = shaders::ShaderLibrary::overlay(parent); + auto parent_handle = parent; + auto overlay_handle = overlay; + + const auto initial_revision = overlay_handle.revision(); + REQUIRE(parent.add_shader("parent/source", "parent").has_value()); + CHECK(parent_handle.contains("parent/source")); + CHECK(overlay_handle.contains("parent/source")); + CHECK(overlay_handle.revision() > initial_revision); + + parent.make_read_only(); + const auto read_only_add = parent_handle.add_shader("blocked", "source"); + REQUIRE_FALSE(read_only_add.has_value()); + CHECK(read_only_add.error() == shaders::ShaderLibraryError::ReadOnly); + + CHECK(shaders::builtin_shader_library().is_read_only()); + auto builtin_handle = shaders::builtin_shader_library(); + const auto builtin_override = builtin_handle.override_shader("mesh/vertex", "blocked"); + REQUIRE_FALSE(builtin_override.has_value()); + CHECK(builtin_override.error() == shaders::ShaderLibraryError::ReadOnly); +} + +TEST_CASE("embedded_shader_sources_are_available", "[visualization][shaders]") { + constexpr std::array paths{ + "flat/vertex", + "flat/fragment", + "examples/triangle/vertex", + "examples/triangle/fragment", + "mesh/vertex", + "mesh/fragment", + "image/vertex", + "image/fragment", + }; + for (const auto path : paths) { + const auto source = shaders::builtin_shader_library().find(path); + REQUIRE(source); + CHECK(source->starts_with("#version")); + CHECK(source->contains("void main")); + } + CHECK_FALSE(shaders::builtin_shader_library().find("missing")); +} + +TEST_CASE("embedded_colormap_registry_is_consistent", "[visualization][shaders]") { + const auto names = shaders::colormap_names(); + + REQUIRE(names.size() == 104); + for (const auto name : names) { + const auto source = shaders::colormap_shader_source(name); + REQUIRE(source.has_value()); + CHECK(source->contains("vec4 colormap(float x)")); + CHECK(balsa::visualization::find_colormap_index(name) >= 0); + } + for (std::size_t i = 0; i < names.size(); ++i) { + for (std::size_t j = i + 1; j < names.size(); ++j) { + CHECK(names[i] != names[j]); + } + } + + CHECK_FALSE(shaders::colormap_shader_source("gnuplot").has_value()); + CHECK_FALSE(shaders::colormap_shader_source("missing").has_value()); + CHECK(balsa::visualization::find_colormap_index("missing") == -1); +} + +TEST_CASE("embedded_shaders_compile", "[visualization][shaders]") { + shaders::AbstractShader compiler; + const auto image_vertex = shaders::builtin_shader_library().find("image/vertex"); + const auto image_fragment = shaders::builtin_shader_library().find("image/fragment"); + const auto mesh_fragment = shaders::builtin_shader_library().find("mesh/fragment"); + REQUIRE(image_vertex); + REQUIRE(image_fragment); + REQUIRE(mesh_fragment); + + CHECK_FALSE(compiler.compile_glsl( + std::string_view{*image_vertex}, + shaders::AbstractShader::ShaderType::Vertex).empty()); + CHECK_FALSE(compiler.compile_glsl( + std::string_view{*image_fragment}, + shaders::AbstractShader::ShaderType::Fragment).empty()); + + std::string fragment{*mesh_fragment}; + const auto version_end = fragment.find('\n'); + REQUIRE(version_end != std::string::npos); + fragment.insert(version_end + 1, *shaders::colormap_shader_source("MATLAB_jet")); + fragment.insert(version_end + 1, "\n#define COLOR_SCALAR_FIELD\n#define SHADING_FLAT\n"); + CHECK_FALSE(compiler.compile_glsl( + fragment, shaders::AbstractShader::ShaderType::Fragment).empty()); +} diff --git a/visualization/tools/meson.build b/visualization/tools/meson.build index 959dc91..da7f10d 100644 --- a/visualization/tools/meson.build +++ b/visualization/tools/meson.build @@ -1,35 +1,41 @@ -cli11_dep = dependency('CLI11', required: false, allow_fallback: true) - -# ── mesh_viewer_glfw ───────────────────────────────────────────────── -mesh_viewer_glfw_deps = [visualization_dep] -mesh_viewer_glfw_args = [] -if cli11_dep.found() - mesh_viewer_glfw_deps += cli11_dep - mesh_viewer_glfw_args += '-DBALSA_HAS_CLI11=1' +if get_option('qt') or (get_option('glfw') and get_option('imgui')) + cli11_dep = dependency('CLI11', required: false, allow_fallback: true) endif -executable('mesh_viewer_glfw', 'mesh_viewer_glfw.cpp', - dependencies: mesh_viewer_glfw_deps, - cpp_args: mesh_viewer_glfw_args) -# ── mesh_viewer_qt ─────────────────────────────────────────────────── -mesh_viewer_qt_deps = [visualization_dep] -mesh_viewer_qt_args = [] -if cli11_dep.found() - mesh_viewer_qt_deps += cli11_dep - mesh_viewer_qt_args += '-DBALSA_HAS_CLI11=1' +if get_option('glfw') and get_option('imgui') + # ── mesh_viewer_glfw ─────────────────────────────────────────────── + mesh_viewer_glfw_deps = [visualization_dep] + mesh_viewer_glfw_args = [] + if cli11_dep.found() + mesh_viewer_glfw_deps += cli11_dep + mesh_viewer_glfw_args += '-DBALSA_HAS_CLI11=1' + endif + executable('mesh_viewer_glfw', 'mesh_viewer_glfw.cpp', + dependencies: mesh_viewer_glfw_deps, + cpp_args: mesh_viewer_glfw_args) + + # ── image_viewer_glfw ────────────────────────────────────────────── + image_viewer_glfw_deps = [visualization_dep] + image_viewer_glfw_args = [] + if cli11_dep.found() + image_viewer_glfw_deps += cli11_dep + image_viewer_glfw_args += '-DBALSA_HAS_CLI11=1' + endif + executable('image_viewer_glfw', 'image_viewer_glfw.cpp', + dependencies: image_viewer_glfw_deps, + cpp_args: image_viewer_glfw_args) endif -executable('mesh_viewer_qt', 'mesh_viewer_qt.cpp', - dependencies: mesh_viewer_qt_deps, - cpp_args: mesh_viewer_qt_args) -# ── image_viewer_glfw ──────────────────────────────────────────────── -image_viewer_glfw_deps = [visualization_dep] -image_viewer_glfw_args = [] -if cli11_dep.found() - image_viewer_glfw_deps += cli11_dep - image_viewer_glfw_args += '-DBALSA_HAS_CLI11=1' +if get_option('qt') + # ── mesh_viewer_qt ───────────────────────────────────────────────── + mesh_viewer_qt_deps = [visualization_dep] + mesh_viewer_qt_args = [] + if cli11_dep.found() + mesh_viewer_qt_deps += cli11_dep + mesh_viewer_qt_args += '-DBALSA_HAS_CLI11=1' + endif + executable('mesh_viewer_qt', 'mesh_viewer_qt.cpp', + dependencies: mesh_viewer_qt_deps, + cpp_args: mesh_viewer_qt_args) endif -executable('image_viewer_glfw', 'image_viewer_glfw.cpp', - dependencies: image_viewer_glfw_deps, - cpp_args: image_viewer_glfw_args)