From 258107039bcbda75a6bef7776b6de25cdaaaebcc Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 6 Jun 2026 14:40:26 -0700 Subject: [PATCH 01/35] Guard disk cache clearing with marker file --- renderlib/CacheManager.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renderlib/CacheManager.h b/renderlib/CacheManager.h index 3d4fff027b..a8f54f70ee 100644 --- a/renderlib/CacheManager.h +++ b/renderlib/CacheManager.h @@ -102,6 +102,11 @@ class CacheManager // Precondition: caller must hold m_mutex. void evictIfNeededLocked(std::uint64_t incomingBytes); void storeImageInMemory(const CacheKey& key, const std::shared_ptr& image); + // Writes a marker file to a directory we manage as our own disk cache root. + // clearDiskCache refuses to delete anything unless this marker is present, + // protecting against accidental wipes of user-typed paths (e.g. "C:\"). + void writeCacheMarker(const std::string& path) const; + bool isAgaveCacheDir(const std::string& path) const; std::shared_ptr loadFromDisk(const CacheKey& key, const CacheConfig& config, const std::string& cacheDir); void storeToDisk(const CacheKey& key, From 9b91e1e7d2cf5778b1986382d09a4a7eb2099f8a Mon Sep 17 00:00:00 2001 From: dmt Date: Sat, 27 Jun 2026 17:25:39 -0700 Subject: [PATCH 02/35] factor context creation out so to remove the qt dependency --- agave_app/renderDialog.cpp | 4 ++-- agave_app/renderDialog.h | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/agave_app/renderDialog.cpp b/agave_app/renderDialog.cpp index 0aef12f5cc..2378c9fa52 100644 --- a/agave_app/renderDialog.cpp +++ b/agave_app/renderDialog.cpp @@ -246,7 +246,7 @@ RenderDialog::RenderDialog(ViewerWindow* borrowedRenderer, , m_renderSettings(renderSettings) , m_scene(scene) , m_camera(ccamera) - , m_glContext(glContext) + , m_glContext(std::make_unique(glContext)) , m_loadSpec(loadSpec) , m_renderThread(nullptr) , m_frameRenderTime(0) @@ -767,7 +767,7 @@ RenderDialog::render() m_camera, m_loadSpec, renderlib::RendererType_Pathtrace, - m_glContext, + m_glContext.get(), mCaptureSettings); onZoomFitClicked(); diff --git a/agave_app/renderDialog.h b/agave_app/renderDialog.h index 0a698299da..f518537ec3 100644 --- a/agave_app/renderDialog.h +++ b/agave_app/renderDialog.h @@ -3,6 +3,7 @@ #include "renderlib/CCamera.h" #include "renderlib/io/FileReader.h" +#include "QtGLContext.h" #include "renderer.h" #include From 2a463097b7b03e0f118f9b2611a611ddcd7b634f Mon Sep 17 00:00:00 2001 From: toloudis Date: Fri, 17 Jul 2026 09:22:30 -0700 Subject: [PATCH 03/35] initial add, with windows building --- CMakeLists.txt | 5 + agave_pyvk/CMakeLists.txt | 48 +++ agave_pyvk/README.md | 19 ++ agave_pyvk/agave_pyvk/__init__.py | 14 + agave_pyvk/agave_pyvk/agave.py | 340 +++++++++++++++++++++ agave_pyvk/agave_pyvk/commandbuffer.py | 62 ++++ agave_pyvk/pyproject.toml | 30 ++ agave_pyvk/src/VulkanRenderer.cpp | 402 +++++++++++++++++++++++++ agave_pyvk/src/VulkanRenderer.h | 53 ++++ agave_pyvk/src/bindings.cpp | 19 ++ agave_pyvk/tests/test_api.py | 70 +++++ 11 files changed, 1062 insertions(+) create mode 100644 agave_pyvk/CMakeLists.txt create mode 100644 agave_pyvk/README.md create mode 100644 agave_pyvk/agave_pyvk/__init__.py create mode 100644 agave_pyvk/agave_pyvk/agave.py create mode 100644 agave_pyvk/agave_pyvk/commandbuffer.py create mode 100644 agave_pyvk/pyproject.toml create mode 100644 agave_pyvk/src/VulkanRenderer.cpp create mode 100644 agave_pyvk/src/VulkanRenderer.h create mode 100644 agave_pyvk/src/bindings.cpp create mode 100644 agave_pyvk/tests/test_api.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 17b9f50134..74b27f11da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,6 +212,11 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # ##################### add_subdirectory(renderlib) +option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" OFF) +if(AGAVE_BUILD_PYVK) + add_subdirectory(agave_pyvk) +endif() + set(INSTALLDIR "${CMAKE_PROJECT_NAME}-install") message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt new file mode 100644 index 0000000000..46032d57cf --- /dev/null +++ b/agave_pyvk/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.24) +project(agave_pyvk VERSION 1.9.0 LANGUAGES CXX) + +find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) +find_package(nanobind CONFIG REQUIRED) + +if(NOT TARGET renderlib) + message(FATAL_ERROR "agave_pyvk must be configured as part of AGAVE after the renderlib target") +endif() +if(NOT AGAVE_HAS_VULKAN) + message(FATAL_ERROR "agave_pyvk requires an AGAVE renderlib target with Vulkan enabled") +endif() + +nanobind_add_module(agave_py2 NB_STATIC src/bindings.cpp src/VulkanRenderer.cpp) +set_target_properties(agave_py2 PROPERTIES OUTPUT_NAME _native) +target_include_directories(agave_py2 PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" + "${CMAKE_CURRENT_SOURCE_DIR}/.." +) +target_link_libraries(agave_py2 PRIVATE renderlib) + +if(WIN32) + install(TARGETS agave_py2 + RUNTIME_DEPENDENCIES + DIRECTORIES "$" + PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" "python[0-9]+\\.dll" + POST_EXCLUDE_REGEXES ".*system32/.*\\.dll" + LIBRARY DESTINATION agave_pyvk + COMPONENT agave_pyvk + RUNTIME DESTINATION agave_pyvk + COMPONENT agave_pyvk) +else() + install(TARGETS agave_py2 + LIBRARY DESTINATION agave_pyvk + COMPONENT agave_pyvk + RUNTIME DESTINATION agave_pyvk + COMPONENT agave_pyvk) +endif() +if(TARGET libCZI) + install(TARGETS libCZI + LIBRARY DESTINATION agave_pyvk + COMPONENT agave_pyvk + RUNTIME DESTINATION agave_pyvk + COMPONENT agave_pyvk) +endif() +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../renderlib/assets/" + DESTINATION agave_pyvk/assets + COMPONENT agave_pyvk) diff --git a/agave_pyvk/README.md b/agave_pyvk/README.md new file mode 100644 index 0000000000..9c572e1dc4 --- /dev/null +++ b/agave_pyvk/README.md @@ -0,0 +1,19 @@ +# agave_pyvk + +`agave_pyvk` is the standalone, headless, in-process Vulkan Python package for +AGAVE. Its `AgaveRenderer` API mirrors `agave_pyclient`, but commands cross a +nanobind boundary and execute synchronously in renderlib. It has no Qt or +WebSocket dependency. + +```python +from agave_pyvk import AgaveRenderer + +with AgaveRenderer(mode="pathtrace") as renderer: + info = renderer.load_data_and_get_info("image.ome.tif") + renderer.set_resolution(1024, 1024) + renderer.session("render.png") + renderer.redraw() +``` + +A Vulkan 1.3-capable driver and the Vulkan SDK are required to build. The +remaining native dependencies are the same ones used by renderlib. diff --git a/agave_pyvk/agave_pyvk/__init__.py b/agave_pyvk/agave_pyvk/__init__.py new file mode 100644 index 0000000000..3b995b59de --- /dev/null +++ b/agave_pyvk/agave_pyvk/__init__.py @@ -0,0 +1,14 @@ +"""Headless in-process Vulkan bindings for AGAVE.""" + +from .agave import AgaveRenderer + +__author__ = "Daniel Toloudis, Allen Institute" +__email__ = "danielt@alleninstitute.org" +__version__ = "1.9.0" + + +def get_module_version() -> str: + return __version__ + + +__all__ = ["AgaveRenderer", "get_module_version"] diff --git a/agave_pyvk/agave_pyvk/agave.py b/agave_pyvk/agave_pyvk/agave.py new file mode 100644 index 0000000000..38c8ffff52 --- /dev/null +++ b/agave_pyvk/agave_pyvk/agave.py @@ -0,0 +1,340 @@ +"""Python API for the in-process headless Vulkan renderer. + +This public surface intentionally mirrors ``agave_pyclient.agave``. The method +implementations are independent and pass typed values directly through +nanobind; there is no WebSocket or serialized protocol buffer. +""" + +import json +import math +from pathlib import Path +from typing import List, Optional + +import numpy +from PIL import Image + +from . import _native +from .commandbuffer import COMMANDS + + +def lerp(startframe, endframe, startval, endval): + x = numpy.linspace( + startframe, endframe, num=endframe - startframe + 1, endpoint=True + ) + y = startval + (endval - startval) * (x - startframe) / (endframe - startframe) + print(y) + + +def rotation_matrix(axis, theta): + axis = numpy.asarray(axis) + axis = axis / math.sqrt(numpy.dot(axis, axis)) + a = math.cos(theta / 2.0) + b, c, d = -axis * math.sin(theta / 2.0) + aa, bb, cc, dd = a * a, b * b, c * c, d * d + bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d + return numpy.array( + [ + [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)], + [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)], + [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc], + ] + ) + + +def rotate_vec(vector, axis, angle): + return numpy.dot(rotation_matrix(axis, angle), vector) + + +def vec_sub(v1, v2): + return [v1[i] - v2[i] for i in range(3)] + + +def vec_add(v1, v2): + return [v1[i] + v2[i] for i in range(3)] + + +def vec_normalize(vector): + magnitude = math.sqrt(sum(value * value for value in vector)) + return [value / magnitude for value in vector] + + +def vec_cross(v1, v2): + return [ + v1[1] * v2[2] - v1[2] * v2[1], + v1[2] * v2[0] - v1[0] * v2[2], + v1[0] * v2[1] - v1[1] * v2[0], + ] + + +def get_vertical_axis(lookdir, up): + eye_direction = vec_normalize(lookdir) + object_up = vec_normalize(up) + sideways = vec_normalize(vec_cross(object_up, eye_direction)) + return vec_normalize(vec_cross(sideways, lookdir)) + + +class AgaveRenderer: + """Synchronous, headless Vulkan implementation of the agave_pyclient API.""" + + def __init__( + self, + url: str = "ws://localhost:1235/", + mode: str = "pathtrace", + agave_path: Optional[str] = None, + auto_launch: bool = True, + launch_retries: int = 10, + launch_retry_delay: float = 1.0, + ) -> None: + del url, agave_path, auto_launch, launch_retries, launch_retry_delay + if mode not in ("pathtrace", "raymarch"): + mode = "pathtrace" + asset_path = Path(_native.__file__).resolve().parent / "assets" + self._renderer = _native.VulkanRenderer(mode, str(asset_path), 0) + self.session_name = "" + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self) -> None: + if self._renderer is not None: + self._renderer.close() + self._renderer = None + + def _execute(self, command, *args): + if self._renderer is None: + raise RuntimeError("Renderer is closed") + result = self._renderer.execute(COMMANDS[command], *args) + if isinstance(result, str): + return json.loads(result) + return result + + def session(self, name: str): + self.session_name = name + return self._execute("SESSION", name) + + def asset_path(self, name: str): + return self._execute("ASSET_PATH", name) + + def load_ome_tif(self, name: str): + return self._execute("LOAD_OME_TIF", name) + + def eye(self, x: float, y: float, z: float): + return self._execute("EYE", x, y, z) + + def target(self, x: float, y: float, z: float): + return self._execute("TARGET", x, y, z) + + def up(self, x: float, y: float, z: float): + return self._execute("UP", x, y, z) + + def aperture(self, x: float): + return self._execute("APERTURE", x) + + def camera_projection(self, projection_type: int, x: float): + return self._execute("CAMERA_PROJECTION", projection_type, x) + + def focaldist(self, x: float): + return self._execute("FOCALDIST", x) + + def exposure(self, x: float): + return self._execute("EXPOSURE", x) + + def mat_diffuse(self, channel: int, r: float, g: float, b: float, a: float): + return self._execute("MAT_DIFFUSE", channel, r, g, b, a) + + def mat_specular(self, channel: int, r: float, g: float, b: float, a: float): + return self._execute("MAT_SPECULAR", channel, r, g, b, a) + + def mat_emissive(self, channel: int, r: float, g: float, b: float, a: float): + return self._execute("MAT_EMISSIVE", channel, r, g, b, a) + + def render_iterations(self, x: int): + return self._execute("RENDER_ITERATIONS", x) + + def stream_mode(self, x: int): + return self._execute("STREAM_MODE", x) + + def redraw(self): + width, height, pixels = self._execute("REDRAW") + image = Image.frombytes("RGBA", (width, height), pixels, "raw", "BGRA") + image.save(self.session_name) + self.session_name = "" + + def set_resolution(self, x: int, y: int): + return self._execute("SET_RESOLUTION", x, y) + + def density(self, x: float): + return self._execute("DENSITY", x) + + def frame_scene(self): + return self._execute("FRAME_SCENE") + + def mat_glossiness(self, channel: int, glossiness: float): + return self._execute("MAT_GLOSSINESS", channel, glossiness) + + def enable_channel(self, channel: int, enabled: int): + return self._execute("ENABLE_CHANNEL", channel, enabled) + + def set_window_level(self, channel: int, window: float, level: float): + return self._execute("SET_WINDOW_LEVEL", channel, window, level) + + def orbit_camera(self, theta: float, phi: float): + return self._execute("ORBIT_CAMERA", theta, phi) + + def trackball_camera(self, theta: float, phi: float): + return self._execute("TRACKBALL_CAMERA", theta, phi) + + def skylight_top_color(self, r: float, g: float, b: float): + return self._execute("SKYLIGHT_TOP_COLOR", r, g, b) + + def skylight_middle_color(self, r: float, g: float, b: float): + return self._execute("SKYLIGHT_MIDDLE_COLOR", r, g, b) + + def skylight_bottom_color(self, r: float, g: float, b: float): + return self._execute("SKYLIGHT_BOTTOM_COLOR", r, g, b) + + def light_pos(self, index: int, r: float, theta: float, phi: float): + return self._execute("LIGHT_POS", index, r, theta, phi) + + def light_color(self, index: int, r: float, g: float, b: float): + return self._execute("LIGHT_COLOR", index, r, g, b) + + def light_size(self, index: int, x: float, y: float): + return self._execute("LIGHT_SIZE", index, x, y) + + def set_clip_region( + self, + minx: float, + maxx: float, + miny: float, + maxy: float, + minz: float, + maxz: float, + ): + return self._execute("SET_CLIP_REGION", minx, maxx, miny, maxy, minz, maxz) + + def set_voxel_scale(self, x: float, y: float, z: float): + return self._execute("SET_VOXEL_SCALE", x, y, z) + + def auto_threshold(self, channel: int, method: int): + return self._execute("AUTO_THRESHOLD", channel, method) + + def set_percentile_threshold(self, channel: int, pct_low: float, pct_high: float): + return self._execute("SET_PERCENTILE_THRESHOLD", channel, pct_low, pct_high) + + def mat_opacity(self, channel: int, opacity: float): + return self._execute("MAT_OPACITY", channel, opacity) + + def set_primary_ray_step_size(self, step_size: float): + return self._execute("SET_PRIMARY_RAY_STEP_SIZE", step_size) + + def set_secondary_ray_step_size(self, step_size: float): + return self._execute("SET_SECONDARY_RAY_STEP_SIZE", step_size) + + def background_color(self, r: float, g: float, b: float): + return self._execute("BACKGROUND_COLOR", r, g, b) + + def set_isovalue_threshold(self, channel: int, isovalue: float, isorange: float): + return self._execute("SET_ISOVALUE_THRESHOLD", channel, isovalue, isorange) + + def set_control_points(self, channel: int, data: List[float]): + return self._execute("SET_CONTROL_POINTS", channel, data) + + def load_volume_from_file(self, path: str, scene: int, time: int): + return self._execute("LOAD_VOLUME_FROM_FILE", path, scene, time) + + def set_time(self, time: int): + return self._execute("SET_TIME", time) + + def bounding_box_color(self, r: float, g: float, b: float): + return self._execute("SET_BOUNDING_BOX_COLOR", r, g, b) + + def show_bounding_box(self, on: int): + return self._execute("SHOW_BOUNDING_BOX", on) + + def load_data( + self, + path: str, + scene: int = 0, + multiresolution_level: int = 0, + time: int = 0, + channels: List[int] = [], + region: List[int] = [], + ): + return self._execute( + "LOAD_DATA", path, scene, multiresolution_level, time, channels, region + ) + + def load_data_and_get_info( + self, + path: str, + scene: int = 0, + multiresolution_level: int = 0, + time: int = 0, + channels: List[int] = [], + region: List[int] = [], + ) -> dict: + return self.load_data( + path, scene, multiresolution_level, time, channels, region + ) + + def show_scale_bar(self, on: int): + return self._execute("SHOW_SCALE_BAR", on) + + def set_flip_axis(self, x: int, y: int, z: int): + return self._execute("SET_FLIP_AXIS", x, y, z) + + def set_interpolation(self, x: int): + return self._execute("SET_INTERPOLATION", x) + + def set_clip_plane(self, x: float, y: float, z: float, d: float): + return self._execute("SET_CLIP_PLANE", x, y, z, d) + + def set_color_ramp(self, channel: int, name: str, data: List[float]): + return self._execute("SET_COLOR_RAMP", channel, name, data) + + def set_min_max_threshold(self, channel: int, min_val: int, max_val: int): + return self._execute("SET_MIN_MAX_THRESHOLD", channel, min_val, max_val) + + def set_skylight_rotation(self, x: float, y: float, z: float, w: float): + return self._execute("SET_SKYLIGHT_ROTATION", x, y, z, w) + + def show_time_stamp(self, on: int): + return self._execute("SHOW_TIME_STAMP", on) + + def set_time_stamp_format(self, format: int): + return self._execute("SET_TIME_STAMP_FORMAT", format) + + def batch_render_turntable( + self, number_of_frames=90, direction=1, output_name="frame", first_frame=0 + ): + if direction not in (1, -1): + return + for i in range(number_of_frames): + self.session(f"{output_name}_{i + first_frame}.png") + self.redraw() + self.trackball_camera(0.0, direction * (360.0 / float(number_of_frames))) + + def batch_render_rocker( + self, + number_of_frames=90, + angle=30, + direction=1, + output_name="frame", + first_frame=0, + ): + if direction not in (1, -1): + return + angle_delta = 4.0 * float(angle) / float(number_of_frames) + for i in range(number_of_frames): + quadrant = (i * 4) // number_of_frames + quadrant_direction = 1 if quadrant in (0, 3) else -1 + self.session(f"{output_name}_{i + first_frame}.png") + self.redraw() + self.trackball_camera(0.0, angle_delta * direction * quadrant_direction) + + +__all__ = ["AgaveRenderer"] diff --git a/agave_pyvk/agave_pyvk/commandbuffer.py b/agave_pyvk/agave_pyvk/commandbuffer.py new file mode 100644 index 0000000000..277a2b3cab --- /dev/null +++ b/agave_pyvk/agave_pyvk/commandbuffer.py @@ -0,0 +1,62 @@ +"""Command identifiers retained for API/source-layout parity with agave_pyclient. + +The headless Vulkan package does not serialize command buffers. ``AgaveRenderer`` +passes typed arguments directly to C++ through nanobind. +""" + +COMMANDS = { + "SESSION": 0, + "ASSET_PATH": 1, + "LOAD_OME_TIF": 2, + "EYE": 3, + "TARGET": 4, + "UP": 5, + "APERTURE": 6, + "CAMERA_PROJECTION": 7, + "FOCALDIST": 8, + "EXPOSURE": 9, + "MAT_DIFFUSE": 10, + "MAT_SPECULAR": 11, + "MAT_EMISSIVE": 12, + "RENDER_ITERATIONS": 13, + "STREAM_MODE": 14, + "REDRAW": 15, + "SET_RESOLUTION": 16, + "DENSITY": 17, + "FRAME_SCENE": 18, + "MAT_GLOSSINESS": 19, + "ENABLE_CHANNEL": 20, + "SET_WINDOW_LEVEL": 21, + "ORBIT_CAMERA": 22, + "SKYLIGHT_TOP_COLOR": 23, + "SKYLIGHT_MIDDLE_COLOR": 24, + "SKYLIGHT_BOTTOM_COLOR": 25, + "LIGHT_POS": 26, + "LIGHT_COLOR": 27, + "LIGHT_SIZE": 28, + "SET_CLIP_REGION": 29, + "SET_VOXEL_SCALE": 30, + "AUTO_THRESHOLD": 31, + "SET_PERCENTILE_THRESHOLD": 32, + "MAT_OPACITY": 33, + "SET_PRIMARY_RAY_STEP_SIZE": 34, + "SET_SECONDARY_RAY_STEP_SIZE": 35, + "BACKGROUND_COLOR": 36, + "SET_ISOVALUE_THRESHOLD": 37, + "SET_CONTROL_POINTS": 38, + "LOAD_VOLUME_FROM_FILE": 39, + "SET_TIME": 40, + "SET_BOUNDING_BOX_COLOR": 41, + "SHOW_BOUNDING_BOX": 42, + "TRACKBALL_CAMERA": 43, + "LOAD_DATA": 44, + "SHOW_SCALE_BAR": 45, + "SET_FLIP_AXIS": 46, + "SET_INTERPOLATION": 47, + "SET_CLIP_PLANE": 48, + "SET_COLOR_RAMP": 49, + "SET_MIN_MAX_THRESHOLD": 50, + "SET_SKYLIGHT_ROTATION": 51, + "SHOW_TIME_STAMP": 52, + "SET_TIME_STAMP_FORMAT": 53, +} diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml new file mode 100644 index 0000000000..c2af19ee81 --- /dev/null +++ b/agave_pyvk/pyproject.toml @@ -0,0 +1,30 @@ +[build-system] +requires = ["scikit-build-core>=0.10", "nanobind>=2.4"] +build-backend = "scikit_build_core.build" + +[project] +name = "agave_pyvk" +version = "1.9.0" +description = "Headless in-process Vulkan Python bindings for AGAVE" +readme = "README.md" +license = { text = "Allen Institute Software License" } +requires-python = ">=3.10" +authors = [{ name = "Daniel Toloudis", email = "danielt@alleninstitute.org" }] +dependencies = ["numpy", "Pillow==12.2.0"] + +[project.optional-dependencies] +test = ["pytest>=5.4.3", "ruff>=0.4"] + +[tool.scikit-build] +cmake.source-dir = ".." +cmake.build-type = "Release" +cmake.define.AGAVE_BUILD_PYVK = "ON" +install.components = ["agave_pyvk"] +wheel.packages = ["agave_pyvk"] + +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +select = ["E", "F", "W", "T10"] +ignore = ["E203", "E402"] diff --git a/agave_pyvk/src/VulkanRenderer.cpp b/agave_pyvk/src/VulkanRenderer.cpp new file mode 100644 index 0000000000..3d0ae914be --- /dev/null +++ b/agave_pyvk/src/VulkanRenderer.cpp @@ -0,0 +1,402 @@ +#include "VulkanRenderer.h" + +#include "renderlib/AppScene.h" +#include "renderlib/BoundingBoxTool.h" +#include "renderlib/CCamera.h" +#include "renderlib/RenderSettings.h" +#include "renderlib/ScaleBarTool.h" +#include "renderlib/SceneView.h" +#include "renderlib/TimeStampTool.h" +#include "renderlib/gfxapi/Backend.h" +#include "renderlib/gfxapi/Framebuffer.h" +#include "renderlib/gfxapi/IGestureRenderer.h" +#include "renderlib/gfxapi/IRenderWindow.h" +#include "renderlib/gfxapi/RenderToFramebuffer.h" +#include "renderlib/gesture/gesture.h" +#include "renderlib/renderlib.h" + +#include +#include + +#include +#include +#include +#include + +namespace nb = nanobind; + +namespace { + +std::mutex s_runtimeMutex; +int s_runtimeUsers = 0; + +template +T +arg(const nb::args& args, size_t index) +{ + return nb::cast(args[index]); +} + +void +requireArgs(int commandId, const nb::args& args, size_t expected) +{ + if (args.size() != expected) { + throw nb::type_error(("command " + std::to_string(commandId) + " expects " + std::to_string(expected) + + " arguments, got " + std::to_string(args.size())) + .c_str()); + } +} + +} // namespace + +VulkanRenderer::VulkanRenderer(const std::string& mode, const std::string& assetPath, int selectedGpu) +{ + initialize(mode, assetPath, selectedGpu); +} + +VulkanRenderer::~VulkanRenderer() +{ + close(); +} + +void +VulkanRenderer::initialize(const std::string& mode, const std::string& assetPath, int selectedGpu) +{ + std::lock_guard lock(s_runtimeMutex); + if (s_runtimeUsers == 0) { + gfxApi::InitParams params; + params.backendKind = gfxApi::BackendKind::Vulkan; + params.assetPath = assetPath; + params.headless = true; + params.selectedGpu = selectedGpu; + if (!renderlib::initialize(params)) { + throw std::runtime_error("Unable to initialize the headless Vulkan backend"); + } + } + ++s_runtimeUsers; + m_ownsRuntime = true; + + try { + m_renderSettings = std::make_unique(); + m_camera = std::make_unique(); + m_camera->m_Film.m_ExposureIterations = 1; + m_camera->m_Film.m_Resolution.SetResX(m_width); + m_camera->m_Film.m_Resolution.SetResY(m_height); + + m_scene = std::make_unique(); + m_scene->initLights(); + + const auto rendererType = mode == "raymarch" ? renderlib::RendererType_Raymarch : renderlib::RendererType_Pathtrace; + m_renderer.reset(renderlib::createRenderer(rendererType, m_renderSettings.get())); + if (!m_renderer) { + throw std::runtime_error("Unable to create the Vulkan volume renderer"); + } + m_renderer->initialize(m_width, m_height); + m_renderer->setScene(m_scene.get()); + + auto* backend = renderlib::graphicsBackend(); + m_framebuffer = backend->createFramebuffer( + { static_cast(m_width), static_cast(m_height), gfxApi::FramebufferColorFormat::Rgba8, true }); + m_gestureRenderer = backend->createGestureRenderer(); + + m_executionContext.m_renderer = this; + m_executionContext.m_renderSettings = m_renderSettings.get(); + m_executionContext.m_appScene = m_scene.get(); + m_executionContext.m_camera = m_camera.get(); + } catch (...) { + m_gestureRenderer.reset(); + m_framebuffer.reset(); + m_renderer.reset(); + m_scene.reset(); + m_camera.reset(); + m_renderSettings.reset(); + if (--s_runtimeUsers == 0) { + renderlib::cleanup(); + } + m_ownsRuntime = false; + throw; + } +} + +void +VulkanRenderer::close() +{ + if (m_closed) { + return; + } + m_closed = true; + + if (m_renderer) { + m_renderer->cleanUpResources(); + } + m_gestureRenderer.reset(); + m_framebuffer.reset(); + m_renderer.reset(); + m_scene.reset(); + m_camera.reset(); + m_renderSettings.reset(); + + std::lock_guard lock(s_runtimeMutex); + if (m_ownsRuntime && --s_runtimeUsers == 0) { + renderlib::cleanup(); + } + m_ownsRuntime = false; +} + +void +VulkanRenderer::setStreamMode(int32_t) +{ + // Synchronous in-process rendering has no streaming mode. +} + +void +VulkanRenderer::resizeGL(int x, int y) +{ + m_width = x; + m_height = y; + m_renderer->resize(static_cast(x), static_cast(y)); + m_framebuffer = renderlib::graphicsBackend()->createFramebuffer( + { static_cast(x), static_cast(y), gfxApi::FramebufferColorFormat::Rgba8, true }); +} + +nb::object +VulkanRenderer::run(Command& command) +{ + if (m_closed) { + throw std::runtime_error("Renderer is closed"); + } + m_executionContext.m_message.clear(); + command.execute(&m_executionContext); + if (!m_executionContext.m_message.empty()) { + return nb::str(m_executionContext.m_message.c_str()); + } + return nb::none(); +} + +nb::object +VulkanRenderer::redraw() +{ + m_camera->Update(); + + SceneView sceneView; + sceneView.viewport.region = { { 0, 0 }, { m_width, m_height } }; + sceneView.camera = *m_camera; + sceneView.scene = m_scene.get(); + sceneView.renderSettings = m_renderSettings.get(); + + Gesture gesture; + if (!gesture.graphics.font.isLoaded()) { + const std::string fontPath = renderlib::assetPath() + "/fonts/Arial.ttf"; + gesture.graphics.font.load(fontPath.c_str()); + } + ScaleBarTool scaleBar; + scaleBar.draw(sceneView, gesture); + BoundingBoxTool boundingBox; + boundingBox.draw(sceneView, gesture); + TimeStampTool timeStamp; + timeStamp.draw(sceneView, gesture); + + gfxApi::renderToFramebuffer(*m_framebuffer, *m_renderer, *m_gestureRenderer, sceneView, gesture.graphics, 0.0f); + + std::vector pixels(static_cast(m_width) * static_cast(m_height) * 4); + m_framebuffer->toImage(pixels.data()); + return nb::make_tuple(m_width, m_height, nb::bytes(reinterpret_cast(pixels.data()), pixels.size())); +} + +nb::object +VulkanRenderer::execute(int commandId, const nb::args& args) +{ +#define EXECUTE_COMMAND(ID, COUNT, TYPE, ...) \ + case ID: { \ + requireArgs(ID, args, COUNT); \ + TYPE command(__VA_ARGS__); \ + return run(command); \ + } + + switch (commandId) { + EXECUTE_COMMAND(0, 1, SessionCommand, SessionCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(1, 1, AssetPathCommand, AssetPathCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(2, 1, LoadOmeTifCommand, LoadOmeTifCommandD{ arg(args, 0) }); + EXECUTE_COMMAND( + 3, 3, SetCameraPosCommand, SetCameraPosCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(4, + 3, + SetCameraTargetCommand, + SetCameraTargetCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND( + 5, 3, SetCameraUpCommand, SetCameraUpCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(6, 1, SetCameraApertureCommand, SetCameraApertureCommandD{ arg(args, 0) }); + EXECUTE_COMMAND( + 7, 2, SetCameraProjectionCommand, SetCameraProjectionCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(8, 1, SetCameraFocalDistanceCommand, SetCameraFocalDistanceCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(9, 1, SetCameraExposureCommand, SetCameraExposureCommandD{ arg(args, 0) }); + EXECUTE_COMMAND( + 10, + 5, + SetDiffuseColorCommand, + SetDiffuseColorCommandD{ + arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3), arg(args, 4) }); + EXECUTE_COMMAND( + 11, + 5, + SetSpecularColorCommand, + SetSpecularColorCommandD{ + arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3), arg(args, 4) }); + EXECUTE_COMMAND( + 12, + 5, + SetEmissiveColorCommand, + SetEmissiveColorCommandD{ + arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3), arg(args, 4) }); + EXECUTE_COMMAND(13, 1, SetRenderIterationsCommand, SetRenderIterationsCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(14, 1, SetStreamModeCommand, SetStreamModeCommandD{ arg(args, 0) }); + case 15: + requireArgs(15, args, 0); + return redraw(); + EXECUTE_COMMAND( + 16, 2, SetResolutionCommand, SetResolutionCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(17, 1, SetDensityCommand, SetDensityCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(18, 0, FrameSceneCommand, FrameSceneCommandD{}); + EXECUTE_COMMAND(19, 2, SetGlossinessCommand, SetGlossinessCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND( + 20, 2, EnableChannelCommand, EnableChannelCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(21, + 3, + SetWindowLevelCommand, + SetWindowLevelCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(22, 2, OrbitCameraCommand, OrbitCameraCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(23, + 3, + SetSkylightTopColorCommand, + SetSkylightTopColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(24, + 3, + SetSkylightMiddleColorCommand, + SetSkylightMiddleColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(25, + 3, + SetSkylightBottomColorCommand, + SetSkylightBottomColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND( + 26, + 4, + SetLightPosCommand, + SetLightPosCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); + EXECUTE_COMMAND( + 27, + 4, + SetLightColorCommand, + SetLightColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); + EXECUTE_COMMAND(28, + 3, + SetLightSizeCommand, + SetLightSizeCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(29, + 6, + SetClipRegionCommand, + SetClipRegionCommandD{ arg(args, 0), + arg(args, 1), + arg(args, 2), + arg(args, 3), + arg(args, 4), + arg(args, 5) }); + EXECUTE_COMMAND(30, + 3, + SetVoxelScaleCommand, + SetVoxelScaleCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND( + 31, 2, AutoThresholdCommand, AutoThresholdCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND( + 32, + 3, + SetPercentileThresholdCommand, + SetPercentileThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(33, 2, SetOpacityCommand, SetOpacityCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(34, 1, SetPrimaryRayStepSizeCommand, SetPrimaryRayStepSizeCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(35, 1, SetSecondaryRayStepSizeCommand, SetSecondaryRayStepSizeCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(36, + 3, + SetBackgroundColorCommand, + SetBackgroundColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(37, + 3, + SetIsovalueThresholdCommand, + SetIsovalueThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(38, + 2, + SetControlPointsCommand, + SetControlPointsCommandD{ arg(args, 0), arg>(args, 1) }); + EXECUTE_COMMAND( + 39, + 3, + LoadVolumeFromFileCommand, + LoadVolumeFromFileCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(40, 1, SetTimeCommand, SetTimeCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(41, + 3, + SetBoundingBoxColorCommand, + SetBoundingBoxColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(42, 1, ShowBoundingBoxCommand, ShowBoundingBoxCommandD{ arg(args, 0) }); + EXECUTE_COMMAND( + 43, 2, TrackballCameraCommand, TrackballCameraCommandD{ arg(args, 0), arg(args, 1) }); + case 44: { + requireArgs(44, args, 6); + const auto region = arg>(args, 5); + LoadDataCommandD data{ arg(args, 0), + arg(args, 1), + arg(args, 2), + arg(args, 3), + arg>(args, 4), + 0, + 0, + 0, + 0, + 0, + 0 }; + if (region.size() == 6) { + data.m_xmin = region[0]; + data.m_xmax = region[1]; + data.m_ymin = region[2]; + data.m_ymax = region[3]; + data.m_zmin = region[4]; + data.m_zmax = region[5]; + } else if (!region.empty()) { + throw nb::value_error("LOAD_DATA region must be empty or contain six integers"); + } + LoadDataCommand command(std::move(data)); + return run(command); + } + EXECUTE_COMMAND(45, 1, ShowScaleBarCommand, ShowScaleBarCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(46, + 3, + SetFlipAxisCommand, + SetFlipAxisCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(47, 1, SetInterpolationCommand, SetInterpolationCommandD{ arg(args, 0) }); + EXECUTE_COMMAND( + 48, + 4, + SetClipPlaneCommand, + SetClipPlaneCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); + EXECUTE_COMMAND( + 49, + 3, + SetColorRampCommand, + SetColorRampCommandD{ arg(args, 0), arg(args, 1), arg>(args, 2) }); + EXECUTE_COMMAND( + 50, + 3, + SetMinMaxThresholdCommand, + SetMinMaxThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(51, + 4, + SetSkylightRotationCommand, + SetSkylightRotationCommandD{ + arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); + EXECUTE_COMMAND(52, 1, ShowTimeStampCommand, ShowTimeStampCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(53, 1, SetTimeStampFormatCommand, SetTimeStampFormatCommandD{ arg(args, 0) }); + default: + throw nb::value_error(("Unknown AGAVE command id " + std::to_string(commandId)).c_str()); + } + +#undef EXECUTE_COMMAND +} diff --git a/agave_pyvk/src/VulkanRenderer.h b/agave_pyvk/src/VulkanRenderer.h new file mode 100644 index 0000000000..1fd6bb7429 --- /dev/null +++ b/agave_pyvk/src/VulkanRenderer.h @@ -0,0 +1,53 @@ +#pragma once + +#include "renderlib/command.h" + +#include + +#include +#include +#include + +class CCamera; +class RenderSettings; +class Scene; + +namespace gfxApi { +class Framebuffer; +class IGestureRenderer; +class IRenderWindow; +} + +class VulkanRenderer final : public RendererCommandInterface +{ +public: + VulkanRenderer(const std::string& mode, const std::string& assetPath, int selectedGpu); + ~VulkanRenderer(); + + VulkanRenderer(const VulkanRenderer&) = delete; + VulkanRenderer& operator=(const VulkanRenderer&) = delete; + + nanobind::object execute(int commandId, const nanobind::args& args); + void close(); + + void setStreamMode(int32_t mode) override; + void resizeGL(int x, int y) override; + +private: + nanobind::object run(Command& command); + nanobind::object redraw(); + void initialize(const std::string& mode, const std::string& assetPath, int selectedGpu); + + int32_t m_width = 1024; + int32_t m_height = 1024; + bool m_closed = false; + bool m_ownsRuntime = false; + + std::unique_ptr m_renderSettings; + std::unique_ptr m_camera; + std::unique_ptr m_scene; + std::unique_ptr m_renderer; + std::unique_ptr m_framebuffer; + std::unique_ptr m_gestureRenderer; + ExecutionContext m_executionContext{}; +}; diff --git a/agave_pyvk/src/bindings.cpp b/agave_pyvk/src/bindings.cpp new file mode 100644 index 0000000000..24344c79ba --- /dev/null +++ b/agave_pyvk/src/bindings.cpp @@ -0,0 +1,19 @@ +#include "VulkanRenderer.h" + +#include +#include + +namespace nb = nanobind; + +NB_MODULE(_native, m) +{ + m.doc() = "Headless Vulkan bindings for AGAVE renderlib"; + + nb::class_(m, "VulkanRenderer") + .def(nb::init(), + nb::arg("mode") = "pathtrace", + nb::arg("asset_path") = "", + nb::arg("gpu") = 0) + .def("execute", &VulkanRenderer::execute) + .def("close", &VulkanRenderer::close); +} diff --git a/agave_pyvk/tests/test_api.py b/agave_pyvk/tests/test_api.py new file mode 100644 index 0000000000..3e79e6d192 --- /dev/null +++ b/agave_pyvk/tests/test_api.py @@ -0,0 +1,70 @@ +import ast +import importlib +import json +import sys +import types +from pathlib import Path + + +class FakeVulkanRenderer: + def __init__(self, mode, asset_path, gpu): + self.constructor_args = (mode, asset_path, gpu) + self.calls = [] + self.closed = False + + def execute(self, command_id, *args): + self.calls.append((command_id, args)) + if command_id == 44: + return json.dumps({"commandId": 44, "x": 10}) + if command_id == 15: + return (1, 1, bytes((0, 0, 255, 255))) + return None + + def close(self): + self.closed = True + + +def load_api(monkeypatch): + fake_native = types.ModuleType("agave_pyvk._native") + fake_native.__file__ = str(Path(__file__).parent / "_native.pyd") + fake_native.VulkanRenderer = FakeVulkanRenderer + monkeypatch.setitem(sys.modules, "agave_pyvk._native", fake_native) + sys.modules.pop("agave_pyvk.agave", None) + return importlib.import_module("agave_pyvk.agave") + + +def public_methods(path): + tree = ast.parse(path.read_text(encoding="utf-8")) + cls = next( + node + for node in tree.body + if isinstance(node, ast.ClassDef) and node.name == "AgaveRenderer" + ) + return { + node.name + for node in cls.body + if isinstance(node, ast.FunctionDef) and not node.name.startswith("_") + } + + +def test_public_api_matches_pyclient(): + repository = Path(__file__).resolve().parents[2] + client = repository / "agave_pyclient" / "agave_pyclient" / "agave.py" + pyvk = repository / "agave_pyvk" / "agave_pyvk" / "agave.py" + assert public_methods(pyvk) == public_methods(client) + + +def test_commands_are_forwarded_directly(monkeypatch): + module = load_api(monkeypatch) + renderer = module.AgaveRenderer(mode="raymarch") + renderer.eye(1.0, 2.0, 3.0) + assert renderer._renderer.calls[-1] == (3, (1.0, 2.0, 3.0)) + + +def test_load_metadata_is_returned_as_dict(monkeypatch): + module = load_api(monkeypatch) + renderer = module.AgaveRenderer() + assert renderer.load_data_and_get_info("volume.ome.tif") == { + "commandId": 44, + "x": 10, + } From 3ed99b2df050c566909d5066289b30ebbb6f5f07 Mon Sep 17 00:00:00 2001 From: toloudis Date: Sat, 18 Jul 2026 11:24:10 -0700 Subject: [PATCH 04/35] build python package --- .github/workflows/build_agave_pyvk.yml | 110 ++++++++++++++++++++ .github/workflows/build_and_test.yml | 3 + .github/workflows/tagged_master_release.yml | 11 ++ 3 files changed, 124 insertions(+) create mode 100644 .github/workflows/build_agave_pyvk.yml diff --git a/.github/workflows/build_agave_pyvk.yml b/.github/workflows/build_agave_pyvk.yml new file mode 100644 index 0000000000..0be0c05697 --- /dev/null +++ b/.github/workflows/build_agave_pyvk.yml @@ -0,0 +1,110 @@ +name: Build agave_pyvk wheels + +on: + workflow_call: + workflow_dispatch: + +jobs: + wheel: + name: ${{ matrix.artifact }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + artifact: agave-pyvk-linux-x86_64 + - os: windows-latest + artifact: agave-pyvk-windows-x86_64 + - os: macos-15 + artifact: agave-pyvk-macos-arm64 + deployment-target: "11.0" + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v6 + with: + submodules: "recursive" + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + # The top-level AGAVE configuration currently discovers Qt even though the + # agave_pyvk target and its installed wheel do not link to Qt. + - name: Install Qt configure-time dependency + uses: jurplel/install-qt-action@v4 + with: + version: 6.9.3 + modules: qtwebsockets qtimageformats + + - name: Install Vulkan SDK + uses: humbletim/install-vulkan-sdk@e7d6e7b937bde4503c7ba5c9584c9dc68f44c4b8 + with: + version: 1.4.350.0 + cache: true + + - name: Install Linux dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libspdlog-dev libglm-dev libgl1-mesa-dev libegl1-mesa-dev libtiff-dev libzstd-dev libcurl4-openssl-dev nasm ninja-build + + - name: Install macOS dependencies + if: contains(matrix.os, 'macos') + run: | + brew install spdlog nasm curl ninja + if [ -d /usr/local/include/openssl ]; then + mv /usr/local/include/openssl /usr/local/include/openssl.bak + fi + shell: bash + + - name: Install Windows dependencies + if: matrix.os == 'windows-latest' + shell: cmd + run: | + choco install nasm -y + call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 + vcpkg install --triplet x64-windows tiff glm zlib libjpeg-turbo liblzma spdlog zstd curl + + - name: Prefer the Git patch executable on Windows + if: matrix.os == 'windows-latest' + run: echo "C:\Program Files\git\usr\bin" >> $env:GITHUB_PATH + + - name: Install wheel build tools + run: python -m pip install --upgrade pip build + + - name: Build Linux wheel + if: matrix.os == 'ubuntu-latest' + env: + CC: gcc-13 + CXX: g++-13 + run: python -m build --wheel agave_pyvk + + - name: Build macOS wheel + if: contains(matrix.os, 'macos') + env: + CC: clang + CXX: clang++ + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment-target }} + Qt6_DIR: ${{ runner.workspace }}/Qt/6.9.3/macos + run: python -m build --wheel agave_pyvk + shell: bash + + - name: Build Windows wheel with MSVC 18 + if: matrix.os == 'windows-latest' + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 + set "CMAKE_GENERATOR=Ninja" + set "CMAKE_ARGS=-DCMAKE_TOOLCHAIN_FILE=%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows" + python -m build --wheel agave_pyvk + + - name: Upload wheel + uses: actions/upload-artifact@v6 + with: + name: ${{ matrix.artifact }} + path: agave_pyvk/dist/*.whl + if-no-files-found: error diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 4e3ebe985b..78612351cb 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -4,6 +4,9 @@ on: branches-ignore: - "master" jobs: + build-agave-pyvk: + uses: ./.github/workflows/build_agave_pyvk.yml + docs: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index f06ca16924..2c2a725691 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -11,6 +11,9 @@ permissions: contents: read jobs: + build-agave-pyvk: + uses: ./.github/workflows/build_agave_pyvk.yml + cmake-build: strategy: matrix: @@ -309,6 +312,7 @@ jobs: needs: [ cmake-build, + build-agave-pyvk, docs, docs_pythonclient, publish_pythonclient, @@ -346,6 +350,12 @@ jobs: with: name: DocumentationPDF path: DocumentationPDF + - name: Get agave_pyvk wheel artifacts + uses: actions/download-artifact@v4 + with: + pattern: agave-pyvk-* + path: agave-pyvk-wheels + merge-multiple: true - name: Version the artifacts run: | mv ${{ github.workspace }}/agave-macos-x86_64/agave-macos-x86_64.dmg ${{ github.workspace }}/agave-macos-x86_64/agave-${{ steps.get_version.outputs.VERSION }}-macos-x86_64.dmg @@ -365,3 +375,4 @@ jobs: ${{ github.workspace }}/agave-macos-arm64/agave-${{ steps.get_version.outputs.VERSION }}-macos-arm64.dmg ${{ github.workspace }}/agave-win/agave-${{ steps.get_version.outputs.VERSION }}-win.exe ${{ github.workspace }}/DocumentationPDF/agave.pdf + ${{ github.workspace }}/agave-pyvk-wheels/*.whl From 604efec92c083a61177e2995386ef30a0fc9d7f6 Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 08:00:04 -0700 Subject: [PATCH 05/35] add nanobind as submodule --- .gitmodules | 3 +++ agave_pyvk/nanobind | 1 + 2 files changed, 4 insertions(+) create mode 160000 agave_pyvk/nanobind diff --git a/.gitmodules b/.gitmodules index c4bc206a46..819edbedcd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "renderlib/libCZI"] path = renderlib/io/libCZI url = https://github.com/ZEISS/libczi.git +[submodule "agave_pyvk/nanobind"] + path = agave_pyvk/nanobind + url = https://github.com/wjakob/nanobind diff --git a/agave_pyvk/nanobind b/agave_pyvk/nanobind new file mode 160000 index 0000000000..ff6a40188a --- /dev/null +++ b/agave_pyvk/nanobind @@ -0,0 +1 @@ +Subproject commit ff6a40188a2f23f0d8c6538c77c83ee6df802f09 From 5d8b347b6e9e42cc4ba2b11b7230078ecfd6f254 Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 11:44:27 -0700 Subject: [PATCH 06/35] iterating on the build system --- .github/workflows/build_agave_pyvk.yml | 110 -------------------- .github/workflows/build_and_test.yml | 41 ++++++-- .github/workflows/tagged_master_release.yml | 46 ++++++-- CMakeLists.txt | 56 ++++++---- agave_pyvk/CMakeLists.txt | 5 +- agave_pyvk/README.md | 23 ++++ agave_pyvk/pyproject.toml | 19 ++++ renderlib/CMakeLists.txt | 4 +- 8 files changed, 155 insertions(+), 149 deletions(-) delete mode 100644 .github/workflows/build_agave_pyvk.yml diff --git a/.github/workflows/build_agave_pyvk.yml b/.github/workflows/build_agave_pyvk.yml deleted file mode 100644 index 0be0c05697..0000000000 --- a/.github/workflows/build_agave_pyvk.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Build agave_pyvk wheels - -on: - workflow_call: - workflow_dispatch: - -jobs: - wheel: - name: ${{ matrix.artifact }} - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - artifact: agave-pyvk-linux-x86_64 - - os: windows-latest - artifact: agave-pyvk-windows-x86_64 - - os: macos-15 - artifact: agave-pyvk-macos-arm64 - deployment-target: "11.0" - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v6 - with: - submodules: "recursive" - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - # The top-level AGAVE configuration currently discovers Qt even though the - # agave_pyvk target and its installed wheel do not link to Qt. - - name: Install Qt configure-time dependency - uses: jurplel/install-qt-action@v4 - with: - version: 6.9.3 - modules: qtwebsockets qtimageformats - - - name: Install Vulkan SDK - uses: humbletim/install-vulkan-sdk@e7d6e7b937bde4503c7ba5c9584c9dc68f44c4b8 - with: - version: 1.4.350.0 - cache: true - - - name: Install Linux dependencies - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install -y libspdlog-dev libglm-dev libgl1-mesa-dev libegl1-mesa-dev libtiff-dev libzstd-dev libcurl4-openssl-dev nasm ninja-build - - - name: Install macOS dependencies - if: contains(matrix.os, 'macos') - run: | - brew install spdlog nasm curl ninja - if [ -d /usr/local/include/openssl ]; then - mv /usr/local/include/openssl /usr/local/include/openssl.bak - fi - shell: bash - - - name: Install Windows dependencies - if: matrix.os == 'windows-latest' - shell: cmd - run: | - choco install nasm -y - call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - vcpkg install --triplet x64-windows tiff glm zlib libjpeg-turbo liblzma spdlog zstd curl - - - name: Prefer the Git patch executable on Windows - if: matrix.os == 'windows-latest' - run: echo "C:\Program Files\git\usr\bin" >> $env:GITHUB_PATH - - - name: Install wheel build tools - run: python -m pip install --upgrade pip build - - - name: Build Linux wheel - if: matrix.os == 'ubuntu-latest' - env: - CC: gcc-13 - CXX: g++-13 - run: python -m build --wheel agave_pyvk - - - name: Build macOS wheel - if: contains(matrix.os, 'macos') - env: - CC: clang - CXX: clang++ - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment-target }} - Qt6_DIR: ${{ runner.workspace }}/Qt/6.9.3/macos - run: python -m build --wheel agave_pyvk - shell: bash - - - name: Build Windows wheel with MSVC 18 - if: matrix.os == 'windows-latest' - shell: cmd - run: | - call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - set "CMAKE_GENERATOR=Ninja" - set "CMAKE_ARGS=-DCMAKE_TOOLCHAIN_FILE=%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows" - python -m build --wheel agave_pyvk - - - name: Upload wheel - uses: actions/upload-artifact@v6 - with: - name: ${{ matrix.artifact }} - path: agave_pyvk/dist/*.whl - if-no-files-found: error diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 78612351cb..d49461eccc 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -4,9 +4,6 @@ on: branches-ignore: - "master" jobs: - build-agave-pyvk: - uses: ./.github/workflows/build_agave_pyvk.yml - docs: runs-on: ubuntu-latest steps: @@ -94,8 +91,10 @@ jobs: artifact: agave-win - os: macos-15 artifact: agave-macos-arm64 + deployment_target: "11.0" - os: macos-15-intel artifact: agave-macos-x86_64 + deployment_target: "10.15" runs-on: ${{ matrix.os }} @@ -103,6 +102,10 @@ jobs: - uses: actions/checkout@v6 with: submodules: "recursive" + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" - name: Install Qt uses: jurplel/install-qt-action@v4 with: @@ -119,7 +122,7 @@ jobs: CC: clang CXX: clang++ run: | - export MACOSX_DEPLOYMENT_TARGET=10.15 + export MACOSX_DEPLOYMENT_TARGET=${{ matrix.deployment_target }} brew install spdlog nasm curl if [ -d /usr/local/include/openssl ]; then mv /usr/local/include/openssl /usr/local/include/openssl.bak @@ -130,11 +133,11 @@ jobs: CC: clang CXX: clang++ run: | - export MACOSX_DEPLOYMENT_TARGET=10.15 + export MACOSX_DEPLOYMENT_TARGET=${{ matrix.deployment_target }} export Qt6_DIR=${{ runner.workspace }}/Qt/6.9.3/macos mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON cmake --build . --target agave_test --config Release cmake --build . --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -160,7 +163,7 @@ jobs: run: | mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON cmake --build . --target agave_test --config Release cmake --build . --target install --config Release shell: bash @@ -199,7 +202,7 @@ jobs: mkdir build cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release .. + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. cmake --build . --parallel --target agave_test --config Release cmake --build . --parallel --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -210,6 +213,28 @@ jobs: with: name: ${{ matrix.artifact }} path: ./build/${{ matrix.artifact }}.exe + - name: Install Python package build frontend + run: python -m pip install build nanobind scikit-build-core + - name: Build agave_pyvk wheel from the existing CMake tree + if: matrix.os != 'windows-latest' + env: + SKBUILD_BUILD_DIR: ${{ github.workspace }}/build + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target }} + run: python -m build --wheel --no-isolation agave_pyvk + - name: Build agave_pyvk wheel from the existing CMake tree with MSVC 18 + if: matrix.os == 'windows-latest' + env: + SKBUILD_BUILD_DIR: ${{ github.workspace }}\build + run: | + call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 + python -m build --wheel --no-isolation agave_pyvk + shell: cmd + - name: Upload agave_pyvk wheel + uses: actions/upload-artifact@v6 + with: + name: agave-pyvk-${{ matrix.artifact }} + path: agave_pyvk/dist/*.whl + if-no-files-found: error lint-and-test-webclient: runs-on: ubuntu-latest diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index 2c2a725691..8260fe458d 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -11,9 +11,6 @@ permissions: contents: read jobs: - build-agave-pyvk: - uses: ./.github/workflows/build_agave_pyvk.yml - cmake-build: strategy: matrix: @@ -24,8 +21,10 @@ jobs: artifact: agave-win - os: macos-15 artifact: agave-macos-arm64 + deployment_target: "11.0" - os: macos-15-intel artifact: agave-macos-x86_64 + deployment_target: "10.15" runs-on: ${{ matrix.os }} @@ -33,6 +32,10 @@ jobs: - uses: actions/checkout@v6 with: submodules: "recursive" + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" - name: Install Qt uses: jurplel/install-qt-action@v4 with: @@ -49,7 +52,7 @@ jobs: CC: clang CXX: clang++ run: | - export MACOSX_DEPLOYMENT_TARGET=10.15 + export MACOSX_DEPLOYMENT_TARGET=${{ matrix.deployment_target }} brew install spdlog nasm curl if [ -d /usr/local/include/openssl ]; then mv /usr/local/include/openssl /usr/local/include/openssl.bak @@ -60,11 +63,11 @@ jobs: CC: clang CXX: clang++ run: | - export MACOSX_DEPLOYMENT_TARGET=10.15 + export MACOSX_DEPLOYMENT_TARGET=${{ matrix.deployment_target }} export Qt6_DIR=${{ runner.workspace }}/Qt/6.9.3/macos mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON cmake --build . --target agave_test --config Release cmake --build . --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -91,7 +94,7 @@ jobs: pwd mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON cmake --build . --target agave_test --config Release cmake --build . --target install --config Release shell: bash @@ -131,7 +134,7 @@ jobs: mkdir build cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release .. + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. cmake --build . --parallel --target agave_test --config Release cmake --build . --parallel --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -142,6 +145,32 @@ jobs: with: name: ${{ matrix.artifact }} path: ./build/${{ matrix.artifact }}.exe + - name: Install cibuildwheel + run: python -m pip install cibuildwheel==3.3.0 + - name: Build Linux agave_pyvk wheel matrix + if: matrix.os == 'ubuntu-latest' + run: python -m cibuildwheel agave_pyvk --output-dir wheelhouse + - name: Build macOS agave_pyvk wheel matrix + if: contains(matrix.os, 'macos') + env: + SKBUILD_BUILD_DIR: ${{ github.workspace }}/build + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target }} + run: python -m cibuildwheel agave_pyvk --output-dir wheelhouse + shell: bash + - name: Build Windows agave_pyvk wheel matrix with MSVC 18 + if: matrix.os == 'windows-latest' + env: + SKBUILD_BUILD_DIR: ${{ github.workspace }}\build + run: | + call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 + python -m cibuildwheel agave_pyvk --output-dir wheelhouse + shell: cmd + - name: Upload agave_pyvk wheels + uses: actions/upload-artifact@v6 + with: + name: agave-pyvk-${{ matrix.artifact }} + path: wheelhouse/*.whl + if-no-files-found: error publish_webclient: needs: cmake-build @@ -312,7 +341,6 @@ jobs: needs: [ cmake-build, - build-agave-pyvk, docs, docs_pythonclient, publish_pythonclient, diff --git a/CMakeLists.txt b/CMakeLists.txt index 74b27f11da..92ff919202 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,14 @@ cmake_minimum_required(VERSION 3.24) include(FetchContent) # Needed to recognize FetchContent_Declare in renderlib -if(APPLE) - set(ENV{MACOSX_DEPLOYMENT_TARGET} "10.15") - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version" FORCE) +if(APPLE) + if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) + if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET}) + set(CMAKE_OSX_DEPLOYMENT_TARGET "$ENV{MACOSX_DEPLOYMENT_TARGET}" CACHE STRING "Minimum OS X deployment version") + else() + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") + endif() + endif() # Ensure compile_commands.json has correct sysroot for clang-tidy if(NOT CMAKE_OSX_SYSROOT) execute_process( @@ -28,11 +33,14 @@ if(POLICY CMP0207) cmake_policy(SET CMP0207 NEW) # file(GET_RUNTIME_DEPENDENCIES) normalizes paths before matching endif() -project( +project( agave VERSION 1.10.0 LANGUAGES C CXX -) +) + +option(AGAVE_BUILD_APP "Build the Qt desktop application and installers" ON) +option(AGAVE_BUILD_TESTS "Build the C++ test suite" ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -55,7 +63,8 @@ set(CMAKE_MODULE_PATH # Find includes in corresponding build directories # set(CMAKE_INCLUDE_CURRENT_DIR ON) -# Find the Qt libraries +if(AGAVE_BUILD_APP) +# Find the Qt libraries # In order for find_package to be successful, Qt 5 must be found below the CMAKE_PREFIX_PATH, # or the Qt6_DIR must be set in the CMake cache to the location of the Qt6WidgetsConfig.cmake # file. The easiest way to use CMake is to set the CMAKE_PREFIX_PATH environment variable to @@ -70,15 +79,19 @@ if(WIN32) set(GUESS_Qt6_DIR C:/Qt/${AGAVE_QT_VERSION}/msvc2022_64 CACHE STRING "Qt6 directory") elseif(APPLE) set(GUESS_Qt6_DIR ~/Qt/${AGAVE_QT_VERSION}/macos) -else() - set(GUESS_Qt6_DIR ~/Qt/${AGAVE_QT_VERSION}/gcc_64) - set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) - set(CMAKE_INSTALL_RPATH "\$ORIGIN") -endif(WIN32) +else() + set(GUESS_Qt6_DIR ~/Qt/${AGAVE_QT_VERSION}/gcc_64) +endif(WIN32) list(INSERT CMAKE_PREFIX_PATH 0 ${GUESS_Qt6_DIR}) -find_package(Qt6 REQUIRED COMPONENTS Widgets Core Gui OpenGL OpenGLWidgets Network WebSockets Xml Svg REQUIRED) +find_package(Qt6 REQUIRED COMPONENTS Widgets Core Gui OpenGL OpenGLWidgets Network WebSockets Xml Svg REQUIRED) +endif() + +if(UNIX AND NOT APPLE) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + set(CMAKE_INSTALL_RPATH "\$ORIGIN") +endif() find_package(spdlog REQUIRED) @@ -226,14 +239,17 @@ message(STATUS "CMAKE_LIBRARY_OUTPUT_DIRECTORY: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY message(STATUS "CMAKE_RUNTIME_OUTPUT_DIRECTORY: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") # ##################### -# UNIT TESTING -# ##################### -add_subdirectory(test) +# UNIT TESTING +# ##################### +if(AGAVE_BUILD_TESTS) +add_subdirectory(test) +endif() # ##################### -# THE FRONT END QT EXE -# ##################### -add_subdirectory(agave_app) +# THE FRONT END QT EXE +# ##################### +if(AGAVE_BUILD_APP) +add_subdirectory(agave_app) # ##################### # Documentation @@ -488,6 +504,8 @@ else() # Linux set(CPACK_PACKAGE_CONTACT "Allen Institute") include(CPack) -endif(WIN32) +endif(WIN32) + +endif() diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt index 46032d57cf..190777d2b1 100644 --- a/agave_pyvk/CMakeLists.txt +++ b/agave_pyvk/CMakeLists.txt @@ -2,7 +2,10 @@ cmake_minimum_required(VERSION 3.24) project(agave_pyvk VERSION 1.9.0 LANGUAGES CXX) find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) -find_package(nanobind CONFIG REQUIRED) +find_package(nanobind CONFIG QUIET) +if(NOT nanobind_FOUND) + add_subdirectory(nanobind) +endif() if(NOT TARGET renderlib) message(FATAL_ERROR "agave_pyvk must be configured as part of AGAVE after the renderlib target") diff --git a/agave_pyvk/README.md b/agave_pyvk/README.md index 9c572e1dc4..ffdeeb47d3 100644 --- a/agave_pyvk/README.md +++ b/agave_pyvk/README.md @@ -17,3 +17,26 @@ with AgaveRenderer(mode="pathtrace") as renderer: A Vulkan 1.3-capable driver and the Vulkan SDK are required to build. The remaining native dependencies are the same ones used by renderlib. + +## Reusing a local CMake build + +Configure the normal AGAVE build with `AGAVE_BUILD_PYVK=ON`. The regular build +then compiles renderlib and the `agave_py2` nanobind target together: + +```powershell +cmake -S . -B D:\agave_build -DAGAVE_BUILD_PYVK=ON +cmake --build D:\agave_build --config Release +``` + +To create a wheel without compiling renderlib again, point scikit-build-core at +that same persistent build tree: + +```powershell +$env:SKBUILD_BUILD_DIR = "D:\agave_build" +python -m pip install build nanobind scikit-build-core +python -m build --wheel --no-isolation agave_pyvk +``` + +Scikit-build-core reconfigures the existing tree for the active Python +interpreter. Renderlib remains up to date; only the Python-version-specific +nanobind sources need to be rebuilt when the interpreter ABI changes. diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index c2af19ee81..935c8b12fa 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -19,9 +19,28 @@ test = ["pytest>=5.4.3", "ruff>=0.4"] cmake.source-dir = ".." cmake.build-type = "Release" cmake.define.AGAVE_BUILD_PYVK = "ON" +cmake.define.AGAVE_BUILD_APP = "OFF" +cmake.define.AGAVE_BUILD_TESTS = "OFF" install.components = ["agave_pyvk"] wheel.packages = ["agave_pyvk"] +[tool.cibuildwheel] +build = "cp3{10,11,12,13,14}-*" +skip = "*-musllinux_*" + +[tool.cibuildwheel.linux] +manylinux-x86_64-image = "manylinux_2_35" +environment = { VULKAN_SDK = "/opt/1.4.350.0/x86_64", SKBUILD_BUILD_DIR = "/tmp/agave-pyvk-build", SKBUILD_CMAKE_SOURCE_DIR = "/project" } +before-all = [ + "apt-get update", + "apt-get install -y cmake curl git libcurl4-openssl-dev libegl1-mesa-dev libgl1-mesa-dev liblzma-dev libspdlog-dev libtiff-dev libx11-dev libxcb1-dev libzstd-dev nasm ninja-build patch pkg-config xz-utils zlib1g-dev", + "curl --fail --location https://sdk.lunarg.com/sdk/download/1.4.350.0/linux/vulkan_sdk.tar.xz --output /tmp/vulkan_sdk.tar.xz", + "echo 'b65f068ab36263559da49d7cacd7e7b9df23824ca8b68ccc522a2b06f5725df2 /tmp/vulkan_sdk.tar.xz' | sha256sum --check", + "mkdir -p /opt && tar -xf /tmp/vulkan_sdk.tar.xz -C /opt", + "cmake -S /project -B /tmp/agave-pyvk-build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=OFF", + "cmake --build /tmp/agave-pyvk-build --target renderlib --parallel", +] + [tool.ruff] line-length = 88 diff --git a/renderlib/CMakeLists.txt b/renderlib/CMakeLists.txt index 23cbbb570b..db9cb8a753 100644 --- a/renderlib/CMakeLists.txt +++ b/renderlib/CMakeLists.txt @@ -3,8 +3,8 @@ configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/version.h" ) -add_library(renderlib "${CMAKE_CURRENT_SOURCE_DIR}/renderlib.cpp") -set_property(TARGET renderlib PROPERTY AUTOMOC ON) +add_library(renderlib "${CMAKE_CURRENT_SOURCE_DIR}/renderlib.cpp") +set_property(TARGET renderlib PROPERTY POSITION_INDEPENDENT_CODE ON) target_include_directories(renderlib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}" From bd91704c9f748bc9bc7666b71c3db599b6a921dc Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 15:00:50 -0700 Subject: [PATCH 07/35] iterating on streamlining the build --- .github/workflows/build_and_test.yml | 22 ++--- .github/workflows/tagged_master_release.yml | 6 +- agave_pyvk/README.md | 21 ++-- agave_pyvk/build_backend.py | 104 ++++++++++++++++++++ agave_pyvk/pyproject.toml | 16 ++- agave_pyvk/tools/build_native.py | 95 ++++++++++++++++++ 6 files changed, 238 insertions(+), 26 deletions(-) create mode 100644 agave_pyvk/build_backend.py create mode 100644 agave_pyvk/tools/build_native.py diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index d49461eccc..b9fcdecd20 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -214,21 +214,17 @@ jobs: name: ${{ matrix.artifact }} path: ./build/${{ matrix.artifact }}.exe - name: Install Python package build frontend - run: python -m pip install build nanobind scikit-build-core - - name: Build agave_pyvk wheel from the existing CMake tree - if: matrix.os != 'windows-latest' + run: python -m pip install build + - name: Stage the agave_py2 CMake install component env: - SKBUILD_BUILD_DIR: ${{ github.workspace }}/build - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target }} - run: python -m build --wheel --no-isolation agave_pyvk - - name: Build agave_pyvk wheel from the existing CMake tree with MSVC 18 - if: matrix.os == 'windows-latest' + AGAVE_PYVK_CMAKE_BUILD_DIR: ${{ github.workspace }}/build + AGAVE_PYVK_PREBUILT_DIR: ${{ github.workspace }}/build/agave-pyvk-stage + run: python agave_pyvk/tools/build_native.py --stage-only + - name: Build agave_pyvk wheel from the staged CMake component env: - SKBUILD_BUILD_DIR: ${{ github.workspace }}\build - run: | - call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - python -m build --wheel --no-isolation agave_pyvk - shell: cmd + AGAVE_PYVK_PREBUILT_DIR: ${{ github.workspace }}/build/agave-pyvk-stage + AGAVE_PYVK_USE_PREBUILT: "1" + run: python -m build --wheel agave_pyvk - name: Upload agave_pyvk wheel uses: actions/upload-artifact@v6 with: diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index 8260fe458d..99e7591ce6 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -153,14 +153,16 @@ jobs: - name: Build macOS agave_pyvk wheel matrix if: contains(matrix.os, 'macos') env: - SKBUILD_BUILD_DIR: ${{ github.workspace }}/build + AGAVE_PYVK_CMAKE_BUILD_DIR: ${{ github.workspace }}/build + AGAVE_PYVK_PREBUILT_DIR: ${{ github.workspace }}/build/agave-pyvk-stage MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target }} run: python -m cibuildwheel agave_pyvk --output-dir wheelhouse shell: bash - name: Build Windows agave_pyvk wheel matrix with MSVC 18 if: matrix.os == 'windows-latest' env: - SKBUILD_BUILD_DIR: ${{ github.workspace }}\build + AGAVE_PYVK_CMAKE_BUILD_DIR: ${{ github.workspace }}\build + AGAVE_PYVK_PREBUILT_DIR: ${{ github.workspace }}\build\agave-pyvk-stage run: | call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 python -m cibuildwheel agave_pyvk --output-dir wheelhouse diff --git a/agave_pyvk/README.md b/agave_pyvk/README.md index ffdeeb47d3..4f02d91706 100644 --- a/agave_pyvk/README.md +++ b/agave_pyvk/README.md @@ -28,15 +28,20 @@ cmake -S . -B D:\agave_build -DAGAVE_BUILD_PYVK=ON cmake --build D:\agave_build --config Release ``` -To create a wheel without compiling renderlib again, point scikit-build-core at -that same persistent build tree: +To create a wheel without compiling renderlib again, have the package helper +build the Python-specific target in that tree and stage its install component: ```powershell -$env:SKBUILD_BUILD_DIR = "D:\agave_build" -python -m pip install build nanobind scikit-build-core -python -m build --wheel --no-isolation agave_pyvk +$env:AGAVE_PYVK_CMAKE_BUILD_DIR = "D:\agave_build" +$env:AGAVE_PYVK_PREBUILT_DIR = "D:\agave_build\agave-pyvk-stage" +python agave_pyvk\tools\build_native.py + +$env:AGAVE_PYVK_USE_PREBUILT = "1" +python -m pip install build +python -m build --wheel agave_pyvk ``` -Scikit-build-core reconfigures the existing tree for the active Python -interpreter. Renderlib remains up to date; only the Python-version-specific -nanobind sources need to be rebuilt when the interpreter ABI changes. +The helper reconfigures the CMake-owned tree for the active Python interpreter. +Renderlib remains up to date; only the Python-version-specific nanobind sources +need to be rebuilt when the interpreter ABI changes. Run it from the same MSVC +x64 Native Tools prompt used for the CMake build on Windows. diff --git a/agave_pyvk/build_backend.py b/agave_pyvk/build_backend.py new file mode 100644 index 0000000000..4d5b51dc28 --- /dev/null +++ b/agave_pyvk/build_backend.py @@ -0,0 +1,104 @@ +"""PEP 517 backend supporting wheels staged by the shared CMake build.""" + +from __future__ import annotations + +import os +from email.message import Message +from pathlib import Path + +from packaging.tags import sys_tags +from scikit_build_core import build as _scikit_build +from wheel.wheelfile import WheelFile + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - Python 3.10 + import tomli as tomllib + + +_PROJECT_ROOT = Path(__file__).resolve().parent + + +def _project_metadata() -> dict: + with (_PROJECT_ROOT / "pyproject.toml").open("rb") as stream: + return tomllib.load(stream)["project"] + + +def _metadata_text(project: dict) -> str: + metadata = Message() + metadata["Metadata-Version"] = "2.1" + metadata["Name"] = project["name"] + metadata["Version"] = project["version"] + metadata["Summary"] = project["description"] + metadata["Requires-Python"] = project["requires-python"] + for author in project.get("authors", []): + metadata["Author"] = author["name"] + metadata["Author-email"] = author["email"] + for dependency in project.get("dependencies", []): + metadata["Requires-Dist"] = dependency + metadata.set_payload("") + return metadata.as_string() + + +def _prebuilt_directory() -> Path: + configured = os.environ.get("AGAVE_PYVK_PREBUILT_DIR") + if configured: + return Path(configured).resolve() + return (_PROJECT_ROOT.parent / "build" / "agave-pyvk-stage").resolve() + + +def _build_prebuilt_wheel(wheel_directory: str) -> str: + package_stage = _prebuilt_directory() / "agave_pyvk" + if not package_stage.is_dir(): + raise RuntimeError(f"staged package directory does not exist: {package_stage}") + + current_tag = next(sys_tags()) + tag = f"{current_tag.interpreter}-{current_tag.abi}-{current_tag.platform}" + project = _project_metadata() + distribution = project["name"].replace("-", "_") + version = project["version"] + wheel_name = f"{distribution}-{version}-{tag}.whl" + dist_info = f"{distribution}-{version}.dist-info" + output = Path(wheel_directory).resolve() + output.mkdir(parents=True, exist_ok=True) + + package_files = {} + for source_root in (_PROJECT_ROOT / "agave_pyvk", package_stage): + for source in source_root.rglob("*"): + if source.is_file() and "__pycache__" not in source.parts: + archive_path = Path("agave_pyvk") / source.relative_to(source_root) + package_files[archive_path.as_posix()] = source + + wheel_metadata = ( + "Wheel-Version: 1.0\n" + "Generator: agave_pyvk prebuilt backend\n" + "Root-Is-Purelib: false\n" + f"Tag: {tag}\n" + ) + with WheelFile(output / wheel_name, "w") as wheel: + for archive_path, source in sorted(package_files.items()): + wheel.write(source, archive_path) + wheel.writestr(f"{dist_info}/METADATA", _metadata_text(project)) + wheel.writestr(f"{dist_info}/WHEEL", wheel_metadata) + return wheel_name + + +def build_wheel( + wheel_directory: str, + config_settings=None, + metadata_directory: str | None = None, +) -> str: + if os.environ.get("AGAVE_PYVK_USE_PREBUILT") == "1": + return _build_prebuilt_wheel(wheel_directory) + return _scikit_build.build_wheel( + wheel_directory, config_settings, metadata_directory + ) + + +build_sdist = _scikit_build.build_sdist +build_editable = _scikit_build.build_editable +get_requires_for_build_wheel = _scikit_build.get_requires_for_build_wheel +get_requires_for_build_sdist = _scikit_build.get_requires_for_build_sdist +get_requires_for_build_editable = _scikit_build.get_requires_for_build_editable +prepare_metadata_for_build_wheel = _scikit_build.prepare_metadata_for_build_wheel +prepare_metadata_for_build_editable = _scikit_build.prepare_metadata_for_build_editable diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index 935c8b12fa..d29f09b525 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -1,6 +1,13 @@ [build-system] -requires = ["scikit-build-core>=0.10", "nanobind>=2.4"] -build-backend = "scikit_build_core.build" +requires = [ + "scikit-build-core>=0.10", + "nanobind>=2.4", + "packaging>=24", + "tomli>=2; python_version<'3.11'", + "wheel>=0.43", +] +build-backend = "build_backend" +backend-path = ["."] [project] name = "agave_pyvk" @@ -27,13 +34,16 @@ wheel.packages = ["agave_pyvk"] [tool.cibuildwheel] build = "cp3{10,11,12,13,14}-*" skip = "*-musllinux_*" +before-build = "python {project}/agave_pyvk/tools/build_native.py" +environment = { AGAVE_PYVK_USE_PREBUILT = "1" } [tool.cibuildwheel.linux] manylinux-x86_64-image = "manylinux_2_35" -environment = { VULKAN_SDK = "/opt/1.4.350.0/x86_64", SKBUILD_BUILD_DIR = "/tmp/agave-pyvk-build", SKBUILD_CMAKE_SOURCE_DIR = "/project" } +environment = { VULKAN_SDK = "/opt/1.4.350.0/x86_64", AGAVE_PYVK_CMAKE_BUILD_DIR = "/tmp/agave-pyvk-build", AGAVE_PYVK_PREBUILT_DIR = "/tmp/agave-pyvk-stage", AGAVE_PYVK_USE_PREBUILT = "1" } before-all = [ "apt-get update", "apt-get install -y cmake curl git libcurl4-openssl-dev libegl1-mesa-dev libgl1-mesa-dev liblzma-dev libspdlog-dev libtiff-dev libx11-dev libxcb1-dev libzstd-dev nasm ninja-build patch pkg-config xz-utils zlib1g-dev", + "python3 -m pip install 'cmake>=3.24' ninja", "curl --fail --location https://sdk.lunarg.com/sdk/download/1.4.350.0/linux/vulkan_sdk.tar.xz --output /tmp/vulkan_sdk.tar.xz", "echo 'b65f068ab36263559da49d7cacd7e7b9df23824ca8b68ccc522a2b06f5725df2 /tmp/vulkan_sdk.tar.xz' | sha256sum --check", "mkdir -p /opt && tar -xf /tmp/vulkan_sdk.tar.xz -C /opt", diff --git a/agave_pyvk/tools/build_native.py b/agave_pyvk/tools/build_native.py new file mode 100644 index 0000000000..56f952ee47 --- /dev/null +++ b/agave_pyvk/tools/build_native.py @@ -0,0 +1,95 @@ +"""Build and stage agave_py2 using a persistent, CMake-owned build tree.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[2] + + +def _configured_path(name: str, default: Path) -> Path: + value = os.environ.get(name) + return Path(value).resolve() if value else default.resolve() + + +def _run(command: list[str]) -> None: + subprocess.run(command, check=True) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--stage-only", + action="store_true", + help="stage an agave_py2 target already built by CMake", + ) + args = parser.parse_args() + + if not args.stage_only and os.name == "nt" and "VSCMD_VER" not in os.environ: + raise RuntimeError( + "build_native.py must run from an MSVC x64 Native Tools prompt" + ) + + build_dir = _configured_path( + "AGAVE_PYVK_CMAKE_BUILD_DIR", PROJECT_ROOT / "build" + ) + stage_dir = _configured_path( + "AGAVE_PYVK_PREBUILT_DIR", build_dir / "agave-pyvk-stage" + ) + + if not args.stage_only: + _run( + [ + "cmake", + "-S", + str(PROJECT_ROOT), + "-B", + str(build_dir), + "-USKBUILD_*", + "-Unanobind_DIR", + "-UPython_*", + "-DAGAVE_BUILD_APP=OFF", + "-DAGAVE_BUILD_TESTS=OFF", + "-DAGAVE_BUILD_PYVK=ON", + f"-DPython_EXECUTABLE={sys.executable}", + f"-DPython_ROOT_DIR={sys.prefix}", + ] + ) + _run( + [ + "cmake", + "--build", + str(build_dir), + "--target", + "agave_py2", + "--config", + "Release", + "--parallel", + ] + ) + + if stage_dir.exists(): + shutil.rmtree(stage_dir) + _run( + [ + "cmake", + "--install", + str(build_dir), + "--config", + "Release", + "--component", + "agave_pyvk", + "--prefix", + str(stage_dir), + ] + ) + + +if __name__ == "__main__": + main() From 1d35942580e8f9f291600171b4877b90c45f4755 Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 15:34:20 -0700 Subject: [PATCH 08/35] push qt and agave_app install info down into the agave_app subdirectory --- CMakeLists.txt | 342 ++++---------------------------------- agave_app/CMakeLists.txt | 25 +++ agave_app/Packaging.cmake | 250 ++++++++++++++++++++++++++++ test/CMakeLists.txt | 1 - 4 files changed, 308 insertions(+), 310 deletions(-) create mode 100644 agave_app/Packaging.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 92ff919202..e79e98382a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,14 +1,14 @@ cmake_minimum_required(VERSION 3.24) include(FetchContent) # Needed to recognize FetchContent_Declare in renderlib -if(APPLE) - if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) - if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET}) - set(CMAKE_OSX_DEPLOYMENT_TARGET "$ENV{MACOSX_DEPLOYMENT_TARGET}" CACHE STRING "Minimum OS X deployment version") - else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") - endif() - endif() +if(APPLE) + if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) + if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET}) + set(CMAKE_OSX_DEPLOYMENT_TARGET "$ENV{MACOSX_DEPLOYMENT_TARGET}" CACHE STRING "Minimum OS X deployment version") + else() + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") + endif() + endif() # Ensure compile_commands.json has correct sysroot for clang-tidy if(NOT CMAKE_OSX_SYSROOT) execute_process( @@ -33,14 +33,14 @@ if(POLICY CMP0207) cmake_policy(SET CMP0207 NEW) # file(GET_RUNTIME_DEPENDENCIES) normalizes paths before matching endif() -project( +project( agave VERSION 1.10.0 LANGUAGES C CXX -) - -option(AGAVE_BUILD_APP "Build the Qt desktop application and installers" ON) -option(AGAVE_BUILD_TESTS "Build the C++ test suite" ON) +) + +option(AGAVE_BUILD_APP "Build the Qt desktop application and installers" ON) +option(AGAVE_BUILD_TESTS "Build the C++ test suite" ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -63,36 +63,11 @@ set(CMAKE_MODULE_PATH # Find includes in corresponding build directories # set(CMAKE_INCLUDE_CURRENT_DIR ON) -if(AGAVE_BUILD_APP) -# Find the Qt libraries -# In order for find_package to be successful, Qt 5 must be found below the CMAKE_PREFIX_PATH, -# or the Qt6_DIR must be set in the CMake cache to the location of the Qt6WidgetsConfig.cmake -# file. The easiest way to use CMake is to set the CMAKE_PREFIX_PATH environment variable to -# the install prefix of Qt 6. -if(DEFINED ENV{Qt6_DIR}) - list(INSERT CMAKE_PREFIX_PATH 0 $ENV{Qt6_DIR}) +if(UNIX AND NOT APPLE) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + set(CMAKE_INSTALL_RPATH "\$ORIGIN") endif() -set(AGAVE_QT_VERSION 6.9.3) - -if(WIN32) - set(GUESS_Qt6_DIR C:/Qt/${AGAVE_QT_VERSION}/msvc2022_64 CACHE STRING "Qt6 directory") -elseif(APPLE) - set(GUESS_Qt6_DIR ~/Qt/${AGAVE_QT_VERSION}/macos) -else() - set(GUESS_Qt6_DIR ~/Qt/${AGAVE_QT_VERSION}/gcc_64) -endif(WIN32) - -list(INSERT CMAKE_PREFIX_PATH 0 ${GUESS_Qt6_DIR}) - -find_package(Qt6 REQUIRED COMPONENTS Widgets Core Gui OpenGL OpenGLWidgets Network WebSockets Xml Svg REQUIRED) -endif() - -if(UNIX AND NOT APPLE) - set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) - set(CMAKE_INSTALL_RPATH "\$ORIGIN") -endif() - find_package(spdlog REQUIRED) # if(MSVC) @@ -225,11 +200,11 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # ##################### add_subdirectory(renderlib) -option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" OFF) -if(AGAVE_BUILD_PYVK) - add_subdirectory(agave_pyvk) -endif() - +option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" OFF) +if(AGAVE_BUILD_PYVK) + add_subdirectory(agave_pyvk) +endif() + set(INSTALLDIR "${CMAKE_PROJECT_NAME}-install") message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") @@ -239,273 +214,22 @@ message(STATUS "CMAKE_LIBRARY_OUTPUT_DIRECTORY: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY message(STATUS "CMAKE_RUNTIME_OUTPUT_DIRECTORY: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") # ##################### -# UNIT TESTING -# ##################### -if(AGAVE_BUILD_TESTS) -add_subdirectory(test) -endif() - +# UNIT TESTING # ##################### -# THE FRONT END QT EXE -# ##################### -if(AGAVE_BUILD_APP) -add_subdirectory(agave_app) - -# ##################### -# Documentation -# ##################### -CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/CMake/conf.py.cmake ${CMAKE_SOURCE_DIR}/docs/conf.py @ONLY) +if(AGAVE_BUILD_TESTS) +add_subdirectory(test) +endif() # ##################### -# CPack +# THE FRONT END QT EXE # ##################### -find_package(Qt6QTiffPlugin 6.9.3 REQUIRED PATHS ${Qt6Gui_DIR}) - -set(CPACK_PACKAGE_VENDOR "Allen Institute") -set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AGAVE is a viewer for 4D multichannel microscopy images, using physically based lighting and rendering.") - -set(CPACK_PACKAGE_NAME "AGAVE") - -install(TARGETS agaveapp - BUNDLE DESTINATION . - RUNTIME DESTINATION ${INSTALLDIR} - LIBRARY DESTINATION ${INSTALLDIR} - DESTINATION ${INSTALLDIR} -) - -if(WIN32) - install(TARGETS agaveapp - RUNTIME_DEPENDENCIES - DIRECTORIES "$" "$" - PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" - POST_EXCLUDE_REGEXES ".*system32/.*\\.dll" - DESTINATION ${INSTALLDIR} - CONFIGURATIONS Release - ) - install(TARGETS agaveapp - RUNTIME_DEPENDENCIES - DIRECTORIES "$" "$" - PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" - POST_EXCLUDE_REGEXES ".*system32/.*\\.dll" - DESTINATION ${INSTALLDIR} - CONFIGURATIONS Debug - ) - install(FILES - ${PROJECT_SOURCE_DIR}/LICENSE.txt - ${PROJECT_SOURCE_DIR}/HELP.txt - $ - $ - $ - DESTINATION ${INSTALLDIR} - CONFIGURATIONS Release - ) - install(FILES - ${PROJECT_SOURCE_DIR}/LICENSE.txt - ${PROJECT_SOURCE_DIR}/HELP.txt - $ - $ - $ - DESTINATION ${INSTALLDIR} - CONFIGURATIONS Debug - ) - install(FILES - ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Arial.ttf - ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Courier_New.ttf - DESTINATION ${INSTALLDIR}/assets/fonts/ - ) - install(FILES - $ - DESTINATION ${INSTALLDIR}/platforms/ - ) - install(FILES - $ - DESTINATION ${INSTALLDIR}/iconengines/ - ) - install(FILES - $ - DESTINATION ${INSTALLDIR}/imageformats/ - ) - install(FILES - $ - DESTINATION ${INSTALLDIR}/imageformats/ - ) - - set(CPACK_GENERATOR "NSIS") - set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_SOURCE_DIR}/CMake/WindowsPreBuild.cmake") - set(CPACK_PACKAGE_INSTALL_DIRECTORY "AGAVE ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") - set(CPACK_PACKAGE_EXECUTABLES "agave" "AGAVE") - set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/agave_app/icons/logo.ico") - set(CPACK_NSIS_MUI_UNIICON "${CMAKE_SOURCE_DIR}/agave_app/icons/logo.ico") - set(CPACK_NSIS_EXECUTABLES_DIRECTORY "agave-install") - set(CPACK_NSIS_MENU_LINKS "https://allen-cell-animated.github.io/agave" "AGAVE Docs") - set(CPACK_NSIS_PACKAGE_NAME "AGAVE ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") - set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " - WriteRegStr HKCR 'agave' '' 'URL:agave' - WriteRegStr HKCR 'agave' 'URL Protocol' '' - WriteRegStr HKCR 'agave\\\\DefaultIcon' '' '\\\"$INSTDIR\\\\agave-install\\\\agave.exe\\\"' - WriteRegStr HKCR 'agave\\\\shell' '' '' - WriteRegStr HKCR 'agave\\\\shell\\\\open' '' '' - WriteRegStr HKCR 'agave\\\\shell\\\\open\\\\command' '' '\\\"$INSTDIR\\\\agave-install\\\\agave.exe\\\" --load \\\"%1\\\"' - ") - set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " - DeleteRegKey HKCR 'agave' - ") - - include(CPack) - -# ############### -elseif(APPLE) - find_package(Qt6QCocoaIntegrationPlugin 6.9.3 REQUIRED PATHS ${Qt6Gui_DIR}) - find_package(Qt6QMacStylePlugin 6.9.3 REQUIRED PATHS ${Qt6Widgets_DIR}) - - # ############### - set(PACKAGE_OSX_TARGET ${CMAKE_OSX_DEPLOYMENT_TARGET}) - add_definitions(-DPACKAGE_OSX_TARGET) +if(AGAVE_BUILD_APP) + add_subdirectory(agave_app) - # based on code from CMake's QtDialog/CMakeLists.txt - macro(install_qt6_plugin _qt_plugin_name _qt_plugins_var _prefix) - get_target_property(_qt_plugin_path "${_qt_plugin_name}" LOCATION) - - if(EXISTS "${_qt_plugin_path}") - get_filename_component(_qt_plugin_file "${_qt_plugin_path}" NAME) - get_filename_component(_qt_plugin_type "${_qt_plugin_path}" PATH) - get_filename_component(_qt_plugin_type "${_qt_plugin_type}" NAME) - set(_qt_plugin_dest "${_prefix}/Contents/PlugIns/${_qt_plugin_type}") - install(FILES "${_qt_plugin_path}" - DESTINATION "${_qt_plugin_dest}") - set(${_qt_plugins_var} - "${${_qt_plugins_var}};\$ENV{DEST_DIR}\${CMAKE_INSTALL_PREFIX}/${_qt_plugin_dest}/${_qt_plugin_file}") - else() - message(FATAL_ERROR "QT plugin ${_qt_plugin_name} not found") - endif() - endmacro() - - install(FILES - $ - $ - $ - $ - $ - $ - $ - $ - $ - $ - DESTINATION agave.app/Contents/Frameworks/ - ) - - # calls cmake install command and also adds to the QT_PLUGINS list for use below - install_qt6_plugin("Qt6::QCocoaIntegrationPlugin" QT_PLUGINS agave.app) - install_qt6_plugin("Qt6::QTiffPlugin" QT_PLUGINS agave.app) - install_qt6_plugin("Qt6::QJpegPlugin" QT_PLUGINS agave.app) - install_qt6_plugin("Qt6::QMacStylePlugin" QT_PLUGINS agave.app) - install_qt6_plugin("Qt6::QSvgIconPlugin" QT_PLUGINS agave.app) - - file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/qt.conf" - "[Paths]\nPlugins = PlugIns\n") - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/qt.conf" - DESTINATION agave.app/Contents/Resources/) - - install(FILES - ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Arial.ttf - ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Courier_New.ttf - DESTINATION agave.app/Contents/Resources/assets/fonts/ - ) - install(FILES - ${PROJECT_SOURCE_DIR}/agave_app/icons/logo.icns - DESTINATION agave.app/Contents/Resources/ - ) - - CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/CMake/Info.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/Info.plist @ONLY) - set_target_properties(agaveapp PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist") - set_target_properties(agaveapp PROPERTIES MACOSX_BUNDLE_ICON_FILE logo.icns) - - # Note Mac specific extension .app - set(APPS "\${CMAKE_INSTALL_PREFIX}/agave.app") - - # Directories to look for dependencies - set(DIRS "${CMAKE_BINARY_DIR}") - - # Path used for searching by FIND_XXX(), with appropriate suffixes added - if(CMAKE_PREFIX_PATH) - foreach(dir ${CMAKE_PREFIX_PATH}) - # note that this should get the necessary Qt lib directories - list(APPEND DIRS "${dir}/bin" "${dir}/lib") - endforeach() - endif() - - # homebrew lib path - list(APPEND DIRS "/opt/homebrew/lib" "/usr/local/lib") - - # Append Qt's lib folder - # list(APPEND DIRS "${QTDIR}/lib") - # list(APPEND DIRS "${Qt6Widgets_DIR}/../..") - - include(InstallRequiredSystemLibraries) - - message(STATUS "APPS: ${APPS}") - message(STATUS "PLUGINS: ${QT_PLUGINS}") - message(STATUS "DIRS: ${DIRS}") - - install(CODE " - include(InstallRequiredSystemLibraries) - include(BundleUtilities) - set(BU_CHMOD_BUNDLE_ITEMS TRUE) - fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/agave.app\" \"${QT_PLUGINS}\" \"${DIRS}\" )") - - set(CPACK_GENERATOR "DragNDrop") - set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_SOURCE_DIR}/CMake/MacOSPreBuild.cmake") - set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/agave_app/icons/dmg-icon.icns") - - include(CPack) - -else() # Linux - find_package(Qt6QXcbIntegrationPlugin 6.9.3 REQUIRED PATHS ${Qt6Gui_DIR}) - - install(FILES - ${PROJECT_SOURCE_DIR}/LICENSE.txt - ${PROJECT_SOURCE_DIR}/HELP.txt - $ - $ - $ - $ - $ - $ - $ - $ - $ - $ - DESTINATION ${INSTALLDIR} - ) - install(TARGETS libCZI LIBRARY DESTINATION ${INSTALLDIR}) - install(FILES - $ - DESTINATION ${INSTALLDIR}/platforms/ - ) - install(FILES - $ - DESTINATION ${INSTALLDIR}/imageformats/ - ) - install(FILES - $ - DESTINATION ${INSTALLDIR}/imageformats/ - ) - install(FILES - $ - DESTINATION ${INSTALLDIR}/iconengines/ - ) - install(FILES - ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Arial.ttf - ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Courier_New.ttf - DESTINATION ${INSTALLDIR}/assets/fonts/ - ) - set(CPACK_GENERATOR "DEB") - set(CPACK_DEBIAN_PACKAGE_NAME "agave") - set(CPACK_PACKAGE_CONTACT "Allen Institute") - - include(CPack) -endif(WIN32) - -endif() + # ##################### + # Documentation + # ##################### + CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/CMake/conf.py.cmake ${CMAKE_SOURCE_DIR}/docs/conf.py @ONLY) +endif() diff --git a/agave_app/CMakeLists.txt b/agave_app/CMakeLists.txt index 9b8d400a70..080a21989e 100644 --- a/agave_app/CMakeLists.txt +++ b/agave_app/CMakeLists.txt @@ -1,3 +1,26 @@ +# Find the Qt libraries +# In order for find_package to be successful, Qt 5 must be found below the CMAKE_PREFIX_PATH, +# or the Qt6_DIR must be set in the CMake cache to the location of the Qt6WidgetsConfig.cmake +# file. The easiest way to use CMake is to set the CMAKE_PREFIX_PATH environment variable to +# the install prefix of Qt 6. +if(DEFINED ENV{Qt6_DIR}) + list(INSERT CMAKE_PREFIX_PATH 0 $ENV{Qt6_DIR}) +endif() + +set(AGAVE_QT_VERSION 6.9.3) + +if(WIN32) + set(GUESS_Qt6_DIR C:/Qt/${AGAVE_QT_VERSION}/msvc2022_64 CACHE STRING "Qt6 directory") +elseif(APPLE) + set(GUESS_Qt6_DIR ~/Qt/${AGAVE_QT_VERSION}/macos) +else() + set(GUESS_Qt6_DIR ~/Qt/${AGAVE_QT_VERSION}/gcc_64) +endif(WIN32) + +list(INSERT CMAKE_PREFIX_PATH 0 ${GUESS_Qt6_DIR}) + +find_package(Qt6 REQUIRED COMPONENTS Widgets Core Gui OpenGL OpenGLWidgets Network WebSockets Xml Svg REQUIRED) + add_executable(agaveapp MACOSX_BUNDLE "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") set_target_properties(agaveapp PROPERTIES OUTPUT_NAME "agave") set_property(TARGET agaveapp PROPERTY AUTOMOC ON) @@ -170,3 +193,5 @@ else() # LINUX "$/assets" ) endif(WIN32) + +include(${CMAKE_CURRENT_SOURCE_DIR}/Packaging.cmake) diff --git a/agave_app/Packaging.cmake b/agave_app/Packaging.cmake new file mode 100644 index 0000000000..2a6bc361b1 --- /dev/null +++ b/agave_app/Packaging.cmake @@ -0,0 +1,250 @@ +# ##################### +# CPack +# ##################### +find_package(Qt6QTiffPlugin 6.9.3 REQUIRED PATHS ${Qt6Gui_DIR}) + +set(CPACK_PACKAGE_VENDOR "Allen Institute") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AGAVE is a viewer for 4D multichannel microscopy images, using physically based lighting and rendering.") + +set(CPACK_PACKAGE_NAME "AGAVE") + +install(TARGETS agaveapp + BUNDLE DESTINATION . + RUNTIME DESTINATION ${INSTALLDIR} + LIBRARY DESTINATION ${INSTALLDIR} + DESTINATION ${INSTALLDIR} +) + +if(WIN32) + install(TARGETS agaveapp + RUNTIME_DEPENDENCIES + DIRECTORIES "$" "$" + PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" + POST_EXCLUDE_REGEXES ".*system32/.*\\.dll" + DESTINATION ${INSTALLDIR} + CONFIGURATIONS Release + ) + install(TARGETS agaveapp + RUNTIME_DEPENDENCIES + DIRECTORIES "$" "$" + PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-" + POST_EXCLUDE_REGEXES ".*system32/.*\\.dll" + DESTINATION ${INSTALLDIR} + CONFIGURATIONS Debug + ) + install(FILES + ${PROJECT_SOURCE_DIR}/LICENSE.txt + ${PROJECT_SOURCE_DIR}/HELP.txt + $ + $ + $ + DESTINATION ${INSTALLDIR} + CONFIGURATIONS Release + ) + install(FILES + ${PROJECT_SOURCE_DIR}/LICENSE.txt + ${PROJECT_SOURCE_DIR}/HELP.txt + $ + $ + $ + DESTINATION ${INSTALLDIR} + CONFIGURATIONS Debug + ) + install(FILES + ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Arial.ttf + ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Courier_New.ttf + DESTINATION ${INSTALLDIR}/assets/fonts/ + ) + install(FILES + $ + DESTINATION ${INSTALLDIR}/platforms/ + ) + install(FILES + $ + DESTINATION ${INSTALLDIR}/iconengines/ + ) + install(FILES + $ + DESTINATION ${INSTALLDIR}/imageformats/ + ) + install(FILES + $ + DESTINATION ${INSTALLDIR}/imageformats/ + ) + + set(CPACK_GENERATOR "NSIS") + set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_SOURCE_DIR}/CMake/WindowsPreBuild.cmake") + set(CPACK_PACKAGE_INSTALL_DIRECTORY "AGAVE ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") + set(CPACK_PACKAGE_EXECUTABLES "agave" "AGAVE") + set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/agave_app/icons/logo.ico") + set(CPACK_NSIS_MUI_UNIICON "${CMAKE_SOURCE_DIR}/agave_app/icons/logo.ico") + set(CPACK_NSIS_EXECUTABLES_DIRECTORY "agave-install") + set(CPACK_NSIS_MENU_LINKS "https://allen-cell-animated.github.io/agave" "AGAVE Docs") + set(CPACK_NSIS_PACKAGE_NAME "AGAVE ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") + set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " + WriteRegStr HKCR 'agave' '' 'URL:agave' + WriteRegStr HKCR 'agave' 'URL Protocol' '' + WriteRegStr HKCR 'agave\\\\DefaultIcon' '' '\\\"$INSTDIR\\\\agave-install\\\\agave.exe\\\"' + WriteRegStr HKCR 'agave\\\\shell' '' '' + WriteRegStr HKCR 'agave\\\\shell\\\\open' '' '' + WriteRegStr HKCR 'agave\\\\shell\\\\open\\\\command' '' '\\\"$INSTDIR\\\\agave-install\\\\agave.exe\\\" --load \\\"%1\\\"' + ") + set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " + DeleteRegKey HKCR 'agave' + ") + + include(CPack) + +# ############### +elseif(APPLE) + find_package(Qt6QCocoaIntegrationPlugin 6.9.3 REQUIRED PATHS ${Qt6Gui_DIR}) + find_package(Qt6QMacStylePlugin 6.9.3 REQUIRED PATHS ${Qt6Widgets_DIR}) + + # ############### + set(PACKAGE_OSX_TARGET ${CMAKE_OSX_DEPLOYMENT_TARGET}) + add_definitions(-DPACKAGE_OSX_TARGET) + + # based on code from CMake's QtDialog/CMakeLists.txt + macro(install_qt6_plugin _qt_plugin_name _qt_plugins_var _prefix) + get_target_property(_qt_plugin_path "${_qt_plugin_name}" LOCATION) + + if(EXISTS "${_qt_plugin_path}") + get_filename_component(_qt_plugin_file "${_qt_plugin_path}" NAME) + get_filename_component(_qt_plugin_type "${_qt_plugin_path}" PATH) + get_filename_component(_qt_plugin_type "${_qt_plugin_type}" NAME) + set(_qt_plugin_dest "${_prefix}/Contents/PlugIns/${_qt_plugin_type}") + install(FILES "${_qt_plugin_path}" + DESTINATION "${_qt_plugin_dest}") + set(${_qt_plugins_var} + "${${_qt_plugins_var}};\$ENV{DEST_DIR}\${CMAKE_INSTALL_PREFIX}/${_qt_plugin_dest}/${_qt_plugin_file}") + else() + message(FATAL_ERROR "QT plugin ${_qt_plugin_name} not found") + endif() + endmacro() + + install(FILES + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + DESTINATION agave.app/Contents/Frameworks/ + ) + + # calls cmake install command and also adds to the QT_PLUGINS list for use below + install_qt6_plugin("Qt6::QCocoaIntegrationPlugin" QT_PLUGINS agave.app) + install_qt6_plugin("Qt6::QTiffPlugin" QT_PLUGINS agave.app) + install_qt6_plugin("Qt6::QJpegPlugin" QT_PLUGINS agave.app) + install_qt6_plugin("Qt6::QMacStylePlugin" QT_PLUGINS agave.app) + install_qt6_plugin("Qt6::QSvgIconPlugin" QT_PLUGINS agave.app) + + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/qt.conf" + "[Paths]\nPlugins = PlugIns\n") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/qt.conf" + DESTINATION agave.app/Contents/Resources/) + + install(FILES + ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Arial.ttf + ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Courier_New.ttf + DESTINATION agave.app/Contents/Resources/assets/fonts/ + ) + install(FILES + ${PROJECT_SOURCE_DIR}/agave_app/icons/logo.icns + DESTINATION agave.app/Contents/Resources/ + ) + + CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/CMake/Info.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/Info.plist @ONLY) + set_target_properties(agaveapp PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist") + set_target_properties(agaveapp PROPERTIES MACOSX_BUNDLE_ICON_FILE logo.icns) + + # Note Mac specific extension .app + set(APPS "\${CMAKE_INSTALL_PREFIX}/agave.app") + + # Directories to look for dependencies + set(DIRS "${CMAKE_BINARY_DIR}") + + # Path used for searching by FIND_XXX(), with appropriate suffixes added + if(CMAKE_PREFIX_PATH) + foreach(dir ${CMAKE_PREFIX_PATH}) + # note that this should get the necessary Qt lib directories + list(APPEND DIRS "${dir}/bin" "${dir}/lib") + endforeach() + endif() + + # homebrew lib path + list(APPEND DIRS "/opt/homebrew/lib" "/usr/local/lib") + + # Append Qt's lib folder + # list(APPEND DIRS "${QTDIR}/lib") + # list(APPEND DIRS "${Qt6Widgets_DIR}/../..") + + include(InstallRequiredSystemLibraries) + + message(STATUS "APPS: ${APPS}") + message(STATUS "PLUGINS: ${QT_PLUGINS}") + message(STATUS "DIRS: ${DIRS}") + + install(CODE " + include(InstallRequiredSystemLibraries) + include(BundleUtilities) + set(BU_CHMOD_BUNDLE_ITEMS TRUE) + fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/agave.app\" \"${QT_PLUGINS}\" \"${DIRS}\" )") + + set(CPACK_GENERATOR "DragNDrop") + set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_SOURCE_DIR}/CMake/MacOSPreBuild.cmake") + set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/agave_app/icons/dmg-icon.icns") + + include(CPack) + +else() # Linux + find_package(Qt6QXcbIntegrationPlugin 6.9.3 REQUIRED PATHS ${Qt6Gui_DIR}) + + install(FILES + ${PROJECT_SOURCE_DIR}/LICENSE.txt + ${PROJECT_SOURCE_DIR}/HELP.txt + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + DESTINATION ${INSTALLDIR} + ) + install(TARGETS libCZI LIBRARY DESTINATION ${INSTALLDIR}) + install(FILES + $ + DESTINATION ${INSTALLDIR}/platforms/ + ) + install(FILES + $ + DESTINATION ${INSTALLDIR}/imageformats/ + ) + install(FILES + $ + DESTINATION ${INSTALLDIR}/imageformats/ + ) + install(FILES + $ + DESTINATION ${INSTALLDIR}/iconengines/ + ) + install(FILES + ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Arial.ttf + ${PROJECT_SOURCE_DIR}/renderlib/assets/fonts/Courier_New.ttf + DESTINATION ${INSTALLDIR}/assets/fonts/ + ) + set(CPACK_GENERATOR "DEB") + set(CPACK_DEBIAN_PACKAGE_NAME "agave") + set(CPACK_PACKAGE_CONTACT "Allen Institute") + + include(CPack) +endif(WIN32) + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 32b210b81d..815b169c83 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -31,7 +31,6 @@ target_sources(agave_test PRIVATE target_link_libraries(agave_test PRIVATE renderlib - Qt6::Core Qt6::Gui Catch2WithMain ) From 71f32bd2cd145a81a2f3c3350e401208ac226f29 Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 16:04:21 -0700 Subject: [PATCH 09/35] nanobind is submoduled --- README.md | 113 +++++++++++++++++++------------ agave_pyvk/CMakeLists.txt | 5 +- agave_pyvk/README.md | 43 +++++++----- agave_pyvk/pyproject.toml | 1 - agave_pyvk/tools/build_native.py | 1 - 5 files changed, 97 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 6afad2823b..bf46a2e638 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,21 @@ AGAVE is a desktop application for viewing multichannel volume data. Several for [Install instructions](INSTALL.md) -## How to build from source: +## How to build from source After cloning this repo, initialize the submodules, which contain a couple of dependency libraries: -``` +```console git submodule update --init ``` -### For WINDOWS: +The commands below build the desktop application, C++ tests, renderlib, and the +standalone `agave_pyvk` native module. Building `agave_pyvk` requires a Vulkan +1.3-capable driver and a Vulkan SDK. CMake uses the SDK named by `VULKAN_SDK`, +or searches the platform's usual Vulkan SDK installation directory. Use Python +3.10 or later when building the standalone package. + +### Windows Make sure you are in an environment where vsvarsall has been run, e.g. a "VS2026 x64 Native Tools Command Prompt" @@ -30,93 +36,112 @@ Make sure you are in an environment where vsvarsall has been run, e.g. a "VS2026 A convenient way to install Perl, NASM, and GNU Patch is with chocolatey. -``` +```console choco install strawberryperl nasm patch ``` **Install Qt LTS 6.9.3.** In your favorite Python virtual environment: -``` +```console pip install aqtinstall aqt install-qt --outputdir C:\Qt windows desktop 6.9.3 win64_msvc2022_64 -m qtwebsockets qtimageformats - ``` Use vcpkg (must use target triplet x64-windows) to install the following: -``` +```console vcpkg install spdlog zlib libjpeg-turbo liblzma tiff zstd curl --triplet x64-windows ``` **Build AGAVE** -``` -mkdir build -cd build -# (vs 2026) -cmake -DCMAKE_TOOLCHAIN_FILE=D:\vcpkg\scripts\buildsystems\vcpkg.cmake -G "Visual Studio 18 2026" -A x64 -DVCPKG_TARGET_TRIPLET=x64-windows .. - -# or, example: ninja in separate build dir! -cmake -DCMAKE_TOOLCHAIN_FILE=C:\Users\%USERNAME%\source\repos\vcpkg\scripts\buildsystems\vcpkg.cmake -G "Ninja Multi-Config" -DVCPKG_TARGET_TRIPLET=x64-windows C:\Users\%USERNAME%\source\repos\allen-cell-animated\agave -cmake --build . --target install +```console +cmake -S . -B build -G "Ninja Multi-Config" ^ + -DCMAKE_TOOLCHAIN_FILE=C:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake ^ + -DVCPKG_TARGET_TRIPLET=x64-windows ^ + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON +cmake --build build --config Release --parallel +cmake --install build --config Release ``` You may need to adjust the vcpkg path depending on your configuration. If you encounter issues during your build, check that all of your dependencies are installed and try again. You can also build to the INSTALL target with Visual Studio by opening the project solution (`agave.sln`). -### For MAC OS: (using homebrew) +### macOS (Homebrew) In your favorite Python virtual environment: -``` +```console pip install aqtinstall aqt install-qt --outputdir ~/Qt mac desktop 6.9.3 -m qtwebsockets qtimageformats export Qt6_DIR=~/Qt/6.9.3/macos -# and then: brew install spdlog libtiff nasm curl -mkdir build -cd build -cmake .. -make -# after make, you should have a runnable agave.app -# or, to build a redistributable bundle: -sudo make install +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON +cmake --build build --parallel +cmake --install build ``` -### For LINUX: +After the build, `build/agave.app` is runnable. To create the redistributable +disk image, run `cpack --config build/CPackConfig.cmake`. + +### Linux Install Qt 6.9.3 in your directory of choice and tell the build where to find it. In your favorite Python virtual environment: -``` - +```console pip install aqtinstall aqt install-qt --outputdir ~/Qt linux desktop 6.9.3 -m qtwebsockets qtimageformats +export Qt6_DIR=~/Qt/6.9.3/gcc_64 -# the next line is needed for CMake +sudo apt install cmake ninja-build libtiff-dev libglm-dev libgl1-mesa-dev \ + libegl1-mesa-dev libspdlog-dev libcurl4-openssl-dev liblzma-dev \ + libzstd-dev zlib1g-dev nasm patch libxcb1-dev -export Qt6_DIR=~/Qt/6.9.3/gcc_64 +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON +cmake --build build --parallel +cmake --install build +``` -sudo apt install libtiff-dev -sudo apt install libglm-dev -sudo apt install libgl1-mesa-dev -sudo apt install libegl1-mesa-dev -sudo apt install libspdlog-dev -sudo apt install libcurl4-openssl-dev -sudo apt install nasm -sudo apt install libxcb-xkb-dev +If cmake fails please refer to the Dockerfile for a more complete list of Linux dependencies. + +### Iterative standalone Python development -mkdir build -cd build -cmake .. -make +The platform commands above create a persistent `build` directory containing +renderlib and the `agave_py2` nanobind target. After changing the Python binding +or renderlib, the following is the shortest build-and-install loop on macOS and +Linux: +```console +python agave_pyvk/tools/build_native.py +AGAVE_PYVK_USE_PREBUILT=1 python -m pip install --force-reinstall --no-deps ./agave_pyvk ``` -If cmake fails please refer to the Dockerfile for a more complete list of Linux dependencies. +Run the equivalent commands from a VS2026 x64 Native Tools Command Prompt on +Windows: + +```console +python agave_pyvk\tools\build_native.py +set "AGAVE_PYVK_USE_PREBUILT=1" +python -m pip install --force-reinstall --no-deps .\agave_pyvk +``` + +`build_native.py` reuses `build`, builds only targets that are out of date, and +stages the `agave_pyvk` install component. The environment variable tells pip +to package that staged native module instead of starting a separate CMake build. +The first install should omit `--no-deps` if NumPy and Pillow are not installed. + +For a headless-only build tree, use the same platform-specific configure command +with these options: + +```console +-DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON +``` ## Versioned Releases diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt index 190777d2b1..107d7e4dc7 100644 --- a/agave_pyvk/CMakeLists.txt +++ b/agave_pyvk/CMakeLists.txt @@ -2,10 +2,7 @@ cmake_minimum_required(VERSION 3.24) project(agave_pyvk VERSION 1.9.0 LANGUAGES CXX) find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) -find_package(nanobind CONFIG QUIET) -if(NOT nanobind_FOUND) - add_subdirectory(nanobind) -endif() +add_subdirectory(nanobind) if(NOT TARGET renderlib) message(FATAL_ERROR "agave_pyvk must be configured as part of AGAVE after the renderlib target") diff --git a/agave_pyvk/README.md b/agave_pyvk/README.md index 4f02d91706..a627b19949 100644 --- a/agave_pyvk/README.md +++ b/agave_pyvk/README.md @@ -20,28 +20,39 @@ remaining native dependencies are the same ones used by renderlib. ## Reusing a local CMake build -Configure the normal AGAVE build with `AGAVE_BUILD_PYVK=ON`. The regular build -then compiles renderlib and the `agave_py2` nanobind target together: +From the repository root, configure a persistent headless build and compile the +`agave_py2` nanobind target together with renderlib: -```powershell -cmake -S . -B D:\agave_build -DAGAVE_BUILD_PYVK=ON -cmake --build D:\agave_build --config Release +```console +cmake -S . -B build -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON +cmake --build build --target agave_py2 --config Release --parallel ``` -To create a wheel without compiling renderlib again, have the package helper -build the Python-specific target in that tree and stage its install component: +On Windows, add the vcpkg toolchain and Ninja Multi-Config arguments shown in +the root `README.md`, and run from a VS2026 x64 Native Tools Command Prompt. +The helper below reconfigures this CMake-owned tree for the active Python +interpreter; renderlib is rebuilt only when it is out of date. -```powershell -$env:AGAVE_PYVK_CMAKE_BUILD_DIR = "D:\agave_build" -$env:AGAVE_PYVK_PREBUILT_DIR = "D:\agave_build\agave-pyvk-stage" +For iterative development, install the staged package directly instead of +creating a wheel file. On macOS and Linux: + +```console +python agave_pyvk/tools/build_native.py +AGAVE_PYVK_USE_PREBUILT=1 python -m pip install --force-reinstall --no-deps ./agave_pyvk +``` + +Or from a VS2026 x64 Native Tools Command Prompt on Windows: + +```console python agave_pyvk\tools\build_native.py +set "AGAVE_PYVK_USE_PREBUILT=1" +python -m pip install --force-reinstall --no-deps .\agave_pyvk +``` + +To produce a wheel file from the same staged build, install the `build` frontend, +set `AGAVE_PYVK_USE_PREBUILT=1` as above, and run: -$env:AGAVE_PYVK_USE_PREBUILT = "1" +```console python -m pip install build python -m build --wheel agave_pyvk ``` - -The helper reconfigures the CMake-owned tree for the active Python interpreter. -Renderlib remains up to date; only the Python-version-specific nanobind sources -need to be rebuilt when the interpreter ABI changes. Run it from the same MSVC -x64 Native Tools prompt used for the CMake build on Windows. diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index d29f09b525..dd7042fee7 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "scikit-build-core>=0.10", - "nanobind>=2.4", "packaging>=24", "tomli>=2; python_version<'3.11'", "wheel>=0.43", diff --git a/agave_pyvk/tools/build_native.py b/agave_pyvk/tools/build_native.py index 56f952ee47..71bcf98b19 100644 --- a/agave_pyvk/tools/build_native.py +++ b/agave_pyvk/tools/build_native.py @@ -52,7 +52,6 @@ def main() -> None: "-B", str(build_dir), "-USKBUILD_*", - "-Unanobind_DIR", "-UPython_*", "-DAGAVE_BUILD_APP=OFF", "-DAGAVE_BUILD_TESTS=OFF", From f893c829435520eceb741b1db910d30fad20181d Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 16:04:49 -0700 Subject: [PATCH 10/35] apple silicon only --- .github/workflows/build_and_test.yml | 3 --- .github/workflows/tagged_master_release.yml | 10 ---------- CMakeLists.txt | 7 ++++--- INSTALL.md | 14 +------------- README.md | 2 +- agave_pyvk/pyproject.toml | 3 +++ 6 files changed, 9 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b9fcdecd20..5b5e8d475b 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -92,9 +92,6 @@ jobs: - os: macos-15 artifact: agave-macos-arm64 deployment_target: "11.0" - - os: macos-15-intel - artifact: agave-macos-x86_64 - deployment_target: "10.15" runs-on: ${{ matrix.os }} diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index 99e7591ce6..6bab5d4c84 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -22,9 +22,6 @@ jobs: - os: macos-15 artifact: agave-macos-arm64 deployment_target: "11.0" - - os: macos-15-intel - artifact: agave-macos-x86_64 - deployment_target: "10.15" runs-on: ${{ matrix.os }} @@ -360,11 +357,6 @@ jobs: run: | echo ${GITHUB_REF/refs\/tags\/v/} echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> "$GITHUB_OUTPUT" - - name: Get mac build artifact x86 - uses: actions/download-artifact@v4 - with: - name: agave-macos-x86_64 - path: agave-macos-x86_64 - name: Get mac build artifact arm64 uses: actions/download-artifact@v4 with: @@ -388,7 +380,6 @@ jobs: merge-multiple: true - name: Version the artifacts run: | - mv ${{ github.workspace }}/agave-macos-x86_64/agave-macos-x86_64.dmg ${{ github.workspace }}/agave-macos-x86_64/agave-${{ steps.get_version.outputs.VERSION }}-macos-x86_64.dmg mv ${{ github.workspace }}/agave-macos-arm64/agave-macos-arm64.dmg ${{ github.workspace }}/agave-macos-arm64/agave-${{ steps.get_version.outputs.VERSION }}-macos-arm64.dmg mv ${{ github.workspace }}/agave-win/agave-win.exe ${{ github.workspace }}/agave-win/agave-${{ steps.get_version.outputs.VERSION }}-win.exe - name: Create Release @@ -401,7 +392,6 @@ jobs: prerelease: true generate_release_notes: true files: | - ${{ github.workspace }}/agave-macos-x86_64/agave-${{ steps.get_version.outputs.VERSION }}-macos-x86_64.dmg ${{ github.workspace }}/agave-macos-arm64/agave-${{ steps.get_version.outputs.VERSION }}-macos-arm64.dmg ${{ github.workspace }}/agave-win/agave-${{ steps.get_version.outputs.VERSION }}-win.exe ${{ github.workspace }}/DocumentationPDF/agave.pdf diff --git a/CMakeLists.txt b/CMakeLists.txt index e79e98382a..689a2b223c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,13 @@ cmake_minimum_required(VERSION 3.24) include(FetchContent) # Needed to recognize FetchContent_Declare in renderlib -if(APPLE) - if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) +if(APPLE) + set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "Supported macOS architecture" FORCE) + if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET}) set(CMAKE_OSX_DEPLOYMENT_TARGET "$ENV{MACOSX_DEPLOYMENT_TARGET}" CACHE STRING "Minimum OS X deployment version") else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") + set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version") endif() endif() # Ensure compile_commands.json has correct sysroot for clang-tidy diff --git a/INSTALL.md b/INSTALL.md index 7fd8928de5..05832e372c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -13,11 +13,7 @@ The first time, you will get a warning from Windows Defender SmartScreen. Click ## MacOS -We provide separate installers for MacOS with Apple Silicon (Macs with M1-M4 processors) and MacOS with Intel processors. - -The Intel (x86-64) version should work on all Macs. However, you will get the best performance by choosing the matching installer. You can check your Mac's processor architecture by clicking the Apple menu, selecting About This Mac. Look for the Processor Name. - -### MacOS with Apple processors +AGAVE supports Apple Silicon Macs. Download the installer `agave-1.10.0-macos-arm64.dmg` and open it. @@ -25,14 +21,6 @@ Drag the agave icon to the Applications folder. You can now run the application from the Applications folder. -### MacOS with Intel processors - -Download the installer `agave-1.10.0-macos-x86-64.dmg` and open it. - -Drag the agave icon to the Applications folder. - -You can now run the application from the Applications folder. - ### MacOS final step If you get a warning that the application is damaged: diff --git a/README.md b/README.md index bf46a2e638..cb7c6d2de8 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ You may need to adjust the vcpkg path depending on your configuration. If you encounter issues during your build, check that all of your dependencies are installed and try again. You can also build to the INSTALL target with Visual Studio by opening the project solution (`agave.sln`). -### macOS (Homebrew) +### macOS Apple Silicon (Homebrew) In your favorite Python virtual environment: diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index dd7042fee7..fa0038e691 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -50,6 +50,9 @@ before-all = [ "cmake --build /tmp/agave-pyvk-build --target renderlib --parallel", ] +[tool.cibuildwheel.macos] +archs = ["arm64"] + [tool.ruff] line-length = 88 From 59a5fed887972a5f80a26be82ff318e97b0395de Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 17:39:20 -0700 Subject: [PATCH 11/35] remove the shaderc check --- CMakeLists.txt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 689a2b223c..1337ef72ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,13 +144,6 @@ if(AGAVE_ENABLE_VULKAN) "Vulkan glslangValidator executable" FORCE) endif() - if(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libshaderc_combined.a") - set(Vulkan_shaderc_combined_LIBRARY - "${AGAVE_VULKAN_SDK_ROOT}/lib/libshaderc_combined.a" - CACHE FILEPATH - "Vulkan shaderc_combined library" - FORCE) - endif() if(WIN32 AND EXISTS "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan-1.lib") set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan-1.lib" CACHE FILEPATH "Vulkan loader library" FORCE) elseif(APPLE AND EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib") @@ -160,7 +153,7 @@ if(AGAVE_ENABLE_VULKAN) endif() endif() find_package(Vulkan COMPONENTS glslc shaderc_combined QUIET) - if(Vulkan_FOUND AND Vulkan_shaderc_combined_FOUND) + if(Vulkan_FOUND AND TARGET Vulkan::shaderc_combined) set(AGAVE_HAS_VULKAN ON) set(AGAVE_VULKAN_RUNTIME_LIBRARY "${Vulkan_LIBRARY}") if(APPLE) From cb356a3f1a9950d89329d9a74f5674547e6d5c68 Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 16:42:42 -0700 Subject: [PATCH 12/35] switch python module build on by default --- CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1337ef72ed..6fbfea14cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,13 @@ cmake_minimum_required(VERSION 3.24) include(FetchContent) # Needed to recognize FetchContent_Declare in renderlib -if(APPLE) - set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "Supported macOS architecture" FORCE) - if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) +if(APPLE) + set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "Supported macOS architecture" FORCE) + if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET}) set(CMAKE_OSX_DEPLOYMENT_TARGET "$ENV{MACOSX_DEPLOYMENT_TARGET}" CACHE STRING "Minimum OS X deployment version") else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version") + set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version") endif() endif() # Ensure compile_commands.json has correct sysroot for clang-tidy @@ -194,7 +194,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # ##################### add_subdirectory(renderlib) -option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" OFF) +option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" ON) if(AGAVE_BUILD_PYVK) add_subdirectory(agave_pyvk) endif() From 3e3b62c5790a74d27ad445f38c9f10dba1ee4c35 Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 17:55:45 -0700 Subject: [PATCH 13/35] diagnosing the build --- .github/workflows/build_and_test.yml | 13 +++++ CMakeLists.txt | 71 ++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 5b5e8d475b..e27ce932e6 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -190,6 +190,15 @@ jobs: if: matrix.os == 'windows-latest' run: | echo "C:\Program Files\git\usr\bin" >> $env:GITHUB_PATH + - name: Diagnose Windows Vulkan SDK installation + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + Write-Host "VULKAN_SDK=$env:VULKAN_SDK" + Write-Host "glslc candidates:" + Get-ChildItem -Path "$env:VULKAN_SDK\Bin" -Filter "glslc*" -ErrorAction Continue + Write-Host "shaderc candidates:" + Get-ChildItem -Path "$env:VULKAN_SDK\Lib" -Filter "*shaderc*" -ErrorAction Continue - name: windows build and test if: matrix.os == 'windows-latest' env: @@ -199,9 +208,13 @@ jobs: mkdir build cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 + if errorlevel 1 exit /b %errorlevel% cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. + if errorlevel 1 exit /b %errorlevel% cmake --build . --parallel --target agave_test --config Release + if errorlevel 1 exit /b %errorlevel% cmake --build . --parallel --config Release + if errorlevel 1 exit /b %errorlevel% cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} shell: cmd - name: Upload windows artifact diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fbfea14cf..0919eb9839 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,9 +93,10 @@ find_package(CURL REQUIRED) find_package(OpenGL) -option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) -set(AGAVE_HAS_VULKAN OFF) -set(AGAVE_VULKAN_RUNTIME_LIBRARY "") +option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) +option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" ON) +set(AGAVE_HAS_VULKAN OFF) +set(AGAVE_VULKAN_RUNTIME_LIBRARY "") if(AGAVE_ENABLE_VULKAN) set(AGAVE_VULKAN_SDK_ROOT "$ENV{VULKAN_SDK}" CACHE PATH "Path to the Vulkan SDK platform directory") if(NOT AGAVE_VULKAN_SDK_ROOT) @@ -149,10 +150,43 @@ if(AGAVE_ENABLE_VULKAN) elseif(APPLE AND EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib") set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib" CACHE FILEPATH "Vulkan loader library" FORCE) elseif(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so") - set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so" CACHE FILEPATH "Vulkan loader library" FORCE) - endif() - endif() - find_package(Vulkan COMPONENTS glslc shaderc_combined QUIET) + set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so" CACHE FILEPATH "Vulkan loader library" FORCE) + endif() + endif() + + file(GLOB _agave_vulkan_glslc_candidates + "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc*" + "${AGAVE_VULKAN_SDK_ROOT}/Bin/glslc*" + ) + file(GLOB _agave_vulkan_shaderc_candidates + "${AGAVE_VULKAN_SDK_ROOT}/lib/*shaderc*" + "${AGAVE_VULKAN_SDK_ROOT}/Lib/*shaderc*" + ) + message(STATUS "Vulkan diagnostics before find_package:") + message(STATUS " VULKAN_SDK environment: $ENV{VULKAN_SDK}") + message(STATUS " AGAVE_VULKAN_SDK_ROOT: ${AGAVE_VULKAN_SDK_ROOT}") + message(STATUS " glslc files in SDK: ${_agave_vulkan_glslc_candidates}") + message(STATUS " shaderc files in SDK: ${_agave_vulkan_shaderc_candidates}") + + find_package(Vulkan COMPONENTS glslc shaderc_combined QUIET) + + if(TARGET Vulkan::shaderc_combined) + set(_agave_vulkan_shaderc_target_found TRUE) + else() + set(_agave_vulkan_shaderc_target_found FALSE) + endif() + message(STATUS "Vulkan diagnostics after find_package:") + message(STATUS " Vulkan_FOUND: ${Vulkan_FOUND}") + message(STATUS " Vulkan_VERSION: ${Vulkan_VERSION}") + message(STATUS " Vulkan_INCLUDE_DIR: ${Vulkan_INCLUDE_DIR}") + message(STATUS " Vulkan_LIBRARY: ${Vulkan_LIBRARY}") + message(STATUS " Vulkan_glslc_FOUND: ${Vulkan_glslc_FOUND}") + message(STATUS " Vulkan_GLSLC_EXECUTABLE: ${Vulkan_GLSLC_EXECUTABLE}") + message(STATUS " Vulkan_shaderc_combined_FOUND: ${Vulkan_shaderc_combined_FOUND}") + message(STATUS " Vulkan_shaderc_combined_LIBRARY: ${Vulkan_shaderc_combined_LIBRARY}") + message(STATUS " Vulkan_shaderc_combined_DEBUG_LIBRARY: ${Vulkan_shaderc_combined_DEBUG_LIBRARY}") + message(STATUS " Vulkan::shaderc_combined target exists: ${_agave_vulkan_shaderc_target_found}") + if(Vulkan_FOUND AND TARGET Vulkan::shaderc_combined) set(AGAVE_HAS_VULKAN ON) set(AGAVE_VULKAN_RUNTIME_LIBRARY "${Vulkan_LIBRARY}") @@ -171,12 +205,20 @@ if(AGAVE_ENABLE_VULKAN) else() message(WARNING "libvulkan_kosmickrisp.dylib was not found; falling back to ${Vulkan_LIBRARY}") endif() - endif() - message(STATUS "Vulkan backend enabled: SDK ${Vulkan_VERSION}") - else() - message(WARNING "AGAVE_ENABLE_VULKAN is ON, but Vulkan SDK with shaderc_combined was not found. Vulkan backend disabled.") - endif() -endif() + endif() + message(STATUS "Vulkan backend enabled: SDK ${Vulkan_VERSION}") + elseif(AGAVE_BUILD_PYVK) + message(FATAL_ERROR + "AGAVE_BUILD_PYVK requires Vulkan, glslc, and shaderc_combined. " + "See the Vulkan diagnostics above for the missing component." + ) + else() + message(WARNING "AGAVE_ENABLE_VULKAN is ON, but a required Vulkan component was not found. Vulkan backend disabled.") + endif() + unset(_agave_vulkan_glslc_candidates) + unset(_agave_vulkan_shaderc_candidates) + unset(_agave_vulkan_shaderc_target_found) +endif() # normally CMAKE_INSTALL_PREFIX is meant to be outside of the build tree if("${CMAKE_BUILD_TYPE}" STREQUAL "") @@ -194,8 +236,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # ##################### add_subdirectory(renderlib) -option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" ON) -if(AGAVE_BUILD_PYVK) +if(AGAVE_BUILD_PYVK) add_subdirectory(agave_pyvk) endif() From 82b3b884cbb69c836425be0cb9e0e08cbd1be9ca Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 19 Jul 2026 19:44:36 -0700 Subject: [PATCH 14/35] more debugging, use a newer fixed vulkan install action --- .github/workflows/build_and_test.yml | 29 +++++++++++++++- .github/workflows/tagged_master_release.yml | 38 ++++++++++++++++++++- CMakeLists.txt | 15 +++++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index e27ce932e6..ce805241b9 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -109,10 +109,33 @@ jobs: version: 6.9.3 modules: qtwebsockets qtimageformats - name: Install Vulkan SDK - uses: humbletim/install-vulkan-sdk@e7d6e7b937bde4503c7ba5c9584c9dc68f44c4b8 + uses: hollykbuck/install-vulkan-sdk@c96510125668208b8cd5069cd667322b0eaedb3d with: version: 1.4.350.0 cache: true + - name: Diagnose Unix Vulkan SDK installation + if: matrix.os != 'windows-latest' + shell: bash + run: | + echo "VULKAN_SDK=$VULKAN_SDK" + echo "Vulkan header candidates:" + if [ -d "$VULKAN_SDK/include/vulkan" ]; then + find "$VULKAN_SDK/include/vulkan" -maxdepth 1 -name "vulkan.h" -print + else + echo "Missing $VULKAN_SDK/include/vulkan" + fi + echo "Vulkan loader and shaderc candidates:" + if [ -d "$VULKAN_SDK/lib" ]; then + find "$VULKAN_SDK/lib" -maxdepth 1 \( -name "libvulkan*" -o -name "*shaderc*" \) -print + else + echo "Missing $VULKAN_SDK/lib" + fi + echo "glslc candidates:" + if [ -d "$VULKAN_SDK/bin" ]; then + find "$VULKAN_SDK/bin" -maxdepth 1 -name "glslc*" -print + else + echo "Missing $VULKAN_SDK/bin" + fi - name: macos install deps if: contains(matrix.os, 'macos') env: @@ -195,6 +218,10 @@ jobs: shell: pwsh run: | Write-Host "VULKAN_SDK=$env:VULKAN_SDK" + Write-Host "Vulkan header candidates:" + Get-Item -Path "$env:VULKAN_SDK\Include\vulkan\vulkan.h" -ErrorAction Continue + Write-Host "Vulkan loader candidates:" + Get-ChildItem -Path "$env:VULKAN_SDK\Lib" -Filter "vulkan*" -ErrorAction Continue Write-Host "glslc candidates:" Get-ChildItem -Path "$env:VULKAN_SDK\Bin" -Filter "glslc*" -ErrorAction Continue Write-Host "shaderc candidates:" diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index 6bab5d4c84..e81d2d59b8 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -39,10 +39,33 @@ jobs: version: 6.9.3 modules: qtwebsockets qtimageformats - name: Install Vulkan SDK - uses: humbletim/install-vulkan-sdk@e7d6e7b937bde4503c7ba5c9584c9dc68f44c4b8 + uses: hollykbuck/install-vulkan-sdk@c96510125668208b8cd5069cd667322b0eaedb3d with: version: 1.4.350.0 cache: true + - name: Diagnose Unix Vulkan SDK installation + if: matrix.os != 'windows-latest' + shell: bash + run: | + echo "VULKAN_SDK=$VULKAN_SDK" + echo "Vulkan header candidates:" + if [ -d "$VULKAN_SDK/include/vulkan" ]; then + find "$VULKAN_SDK/include/vulkan" -maxdepth 1 -name "vulkan.h" -print + else + echo "Missing $VULKAN_SDK/include/vulkan" + fi + echo "Vulkan loader and shaderc candidates:" + if [ -d "$VULKAN_SDK/lib" ]; then + find "$VULKAN_SDK/lib" -maxdepth 1 \( -name "libvulkan*" -o -name "*shaderc*" \) -print + else + echo "Missing $VULKAN_SDK/lib" + fi + echo "glslc candidates:" + if [ -d "$VULKAN_SDK/bin" ]; then + find "$VULKAN_SDK/bin" -maxdepth 1 -name "glslc*" -print + else + echo "Missing $VULKAN_SDK/bin" + fi - name: macos install deps if: contains(matrix.os, 'macos') env: @@ -122,6 +145,19 @@ jobs: if: matrix.os == 'windows-latest' run: | echo "C:\Program Files\git\usr\bin" >> $env:GITHUB_PATH + - name: Diagnose Windows Vulkan SDK installation + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + Write-Host "VULKAN_SDK=$env:VULKAN_SDK" + Write-Host "Vulkan header candidates:" + Get-Item -Path "$env:VULKAN_SDK\Include\vulkan\vulkan.h" -ErrorAction Continue + Write-Host "Vulkan loader candidates:" + Get-ChildItem -Path "$env:VULKAN_SDK\Lib" -Filter "vulkan*" -ErrorAction Continue + Write-Host "glslc candidates:" + Get-ChildItem -Path "$env:VULKAN_SDK\Bin" -Filter "glslc*" -ErrorAction Continue + Write-Host "shaderc candidates:" + Get-ChildItem -Path "$env:VULKAN_SDK\Lib" -Filter "*shaderc*" -ErrorAction Continue - name: windows build and test if: matrix.os == 'windows-latest' env: diff --git a/CMakeLists.txt b/CMakeLists.txt index 0919eb9839..f3e8e6680b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,9 +162,19 @@ if(AGAVE_ENABLE_VULKAN) "${AGAVE_VULKAN_SDK_ROOT}/lib/*shaderc*" "${AGAVE_VULKAN_SDK_ROOT}/Lib/*shaderc*" ) + file(GLOB _agave_vulkan_header_candidates + "${AGAVE_VULKAN_SDK_ROOT}/include/vulkan/vulkan.h" + "${AGAVE_VULKAN_SDK_ROOT}/Include/vulkan/vulkan.h" + ) + file(GLOB _agave_vulkan_loader_candidates + "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan*" + "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan*" + ) message(STATUS "Vulkan diagnostics before find_package:") message(STATUS " VULKAN_SDK environment: $ENV{VULKAN_SDK}") message(STATUS " AGAVE_VULKAN_SDK_ROOT: ${AGAVE_VULKAN_SDK_ROOT}") + message(STATUS " Vulkan header files in SDK: ${_agave_vulkan_header_candidates}") + message(STATUS " Vulkan loader files in SDK: ${_agave_vulkan_loader_candidates}") message(STATUS " glslc files in SDK: ${_agave_vulkan_glslc_candidates}") message(STATUS " shaderc files in SDK: ${_agave_vulkan_shaderc_candidates}") @@ -179,12 +189,13 @@ if(AGAVE_ENABLE_VULKAN) message(STATUS " Vulkan_FOUND: ${Vulkan_FOUND}") message(STATUS " Vulkan_VERSION: ${Vulkan_VERSION}") message(STATUS " Vulkan_INCLUDE_DIR: ${Vulkan_INCLUDE_DIR}") + message(STATUS " Vulkan_INCLUDE_DIRS: ${Vulkan_INCLUDE_DIRS}") message(STATUS " Vulkan_LIBRARY: ${Vulkan_LIBRARY}") + message(STATUS " Vulkan_LIBRARIES: ${Vulkan_LIBRARIES}") message(STATUS " Vulkan_glslc_FOUND: ${Vulkan_glslc_FOUND}") message(STATUS " Vulkan_GLSLC_EXECUTABLE: ${Vulkan_GLSLC_EXECUTABLE}") message(STATUS " Vulkan_shaderc_combined_FOUND: ${Vulkan_shaderc_combined_FOUND}") message(STATUS " Vulkan_shaderc_combined_LIBRARY: ${Vulkan_shaderc_combined_LIBRARY}") - message(STATUS " Vulkan_shaderc_combined_DEBUG_LIBRARY: ${Vulkan_shaderc_combined_DEBUG_LIBRARY}") message(STATUS " Vulkan::shaderc_combined target exists: ${_agave_vulkan_shaderc_target_found}") if(Vulkan_FOUND AND TARGET Vulkan::shaderc_combined) @@ -217,6 +228,8 @@ if(AGAVE_ENABLE_VULKAN) endif() unset(_agave_vulkan_glslc_candidates) unset(_agave_vulkan_shaderc_candidates) + unset(_agave_vulkan_header_candidates) + unset(_agave_vulkan_loader_candidates) unset(_agave_vulkan_shaderc_target_found) endif() From 93a8d72162ef24047fd6d8549b68058a3da63dcd Mon Sep 17 00:00:00 2001 From: toloudis Date: Mon, 20 Jul 2026 08:22:33 -0700 Subject: [PATCH 15/35] find_package not quiet --- CMakeLists.txt | 140 ++++++++++++++++++++++++------------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f3e8e6680b..0d735a4ec6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,10 +93,10 @@ find_package(CURL REQUIRED) find_package(OpenGL) -option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) -option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" ON) -set(AGAVE_HAS_VULKAN OFF) -set(AGAVE_VULKAN_RUNTIME_LIBRARY "") +option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) +option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" ON) +set(AGAVE_HAS_VULKAN OFF) +set(AGAVE_VULKAN_RUNTIME_LIBRARY "") if(AGAVE_ENABLE_VULKAN) set(AGAVE_VULKAN_SDK_ROOT "$ENV{VULKAN_SDK}" CACHE PATH "Path to the Vulkan SDK platform directory") if(NOT AGAVE_VULKAN_SDK_ROOT) @@ -150,55 +150,55 @@ if(AGAVE_ENABLE_VULKAN) elseif(APPLE AND EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib") set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib" CACHE FILEPATH "Vulkan loader library" FORCE) elseif(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so") - set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so" CACHE FILEPATH "Vulkan loader library" FORCE) - endif() - endif() - - file(GLOB _agave_vulkan_glslc_candidates - "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc*" - "${AGAVE_VULKAN_SDK_ROOT}/Bin/glslc*" - ) - file(GLOB _agave_vulkan_shaderc_candidates - "${AGAVE_VULKAN_SDK_ROOT}/lib/*shaderc*" - "${AGAVE_VULKAN_SDK_ROOT}/Lib/*shaderc*" - ) - file(GLOB _agave_vulkan_header_candidates - "${AGAVE_VULKAN_SDK_ROOT}/include/vulkan/vulkan.h" - "${AGAVE_VULKAN_SDK_ROOT}/Include/vulkan/vulkan.h" - ) - file(GLOB _agave_vulkan_loader_candidates - "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan*" - "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan*" - ) - message(STATUS "Vulkan diagnostics before find_package:") - message(STATUS " VULKAN_SDK environment: $ENV{VULKAN_SDK}") - message(STATUS " AGAVE_VULKAN_SDK_ROOT: ${AGAVE_VULKAN_SDK_ROOT}") - message(STATUS " Vulkan header files in SDK: ${_agave_vulkan_header_candidates}") - message(STATUS " Vulkan loader files in SDK: ${_agave_vulkan_loader_candidates}") - message(STATUS " glslc files in SDK: ${_agave_vulkan_glslc_candidates}") - message(STATUS " shaderc files in SDK: ${_agave_vulkan_shaderc_candidates}") - - find_package(Vulkan COMPONENTS glslc shaderc_combined QUIET) - - if(TARGET Vulkan::shaderc_combined) - set(_agave_vulkan_shaderc_target_found TRUE) - else() - set(_agave_vulkan_shaderc_target_found FALSE) - endif() - message(STATUS "Vulkan diagnostics after find_package:") - message(STATUS " Vulkan_FOUND: ${Vulkan_FOUND}") - message(STATUS " Vulkan_VERSION: ${Vulkan_VERSION}") - message(STATUS " Vulkan_INCLUDE_DIR: ${Vulkan_INCLUDE_DIR}") - message(STATUS " Vulkan_INCLUDE_DIRS: ${Vulkan_INCLUDE_DIRS}") - message(STATUS " Vulkan_LIBRARY: ${Vulkan_LIBRARY}") - message(STATUS " Vulkan_LIBRARIES: ${Vulkan_LIBRARIES}") - message(STATUS " Vulkan_glslc_FOUND: ${Vulkan_glslc_FOUND}") - message(STATUS " Vulkan_GLSLC_EXECUTABLE: ${Vulkan_GLSLC_EXECUTABLE}") - message(STATUS " Vulkan_shaderc_combined_FOUND: ${Vulkan_shaderc_combined_FOUND}") - message(STATUS " Vulkan_shaderc_combined_LIBRARY: ${Vulkan_shaderc_combined_LIBRARY}") - message(STATUS " Vulkan::shaderc_combined target exists: ${_agave_vulkan_shaderc_target_found}") - - if(Vulkan_FOUND AND TARGET Vulkan::shaderc_combined) + set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so" CACHE FILEPATH "Vulkan loader library" FORCE) + endif() + endif() + + file(GLOB _agave_vulkan_glslc_candidates + "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc*" + "${AGAVE_VULKAN_SDK_ROOT}/Bin/glslc*" + ) + file(GLOB _agave_vulkan_shaderc_candidates + "${AGAVE_VULKAN_SDK_ROOT}/lib/*shaderc*" + "${AGAVE_VULKAN_SDK_ROOT}/Lib/*shaderc*" + ) + file(GLOB _agave_vulkan_header_candidates + "${AGAVE_VULKAN_SDK_ROOT}/include/vulkan/vulkan.h" + "${AGAVE_VULKAN_SDK_ROOT}/Include/vulkan/vulkan.h" + ) + file(GLOB _agave_vulkan_loader_candidates + "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan*" + "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan*" + ) + message(STATUS "Vulkan diagnostics before find_package:") + message(STATUS " VULKAN_SDK environment: $ENV{VULKAN_SDK}") + message(STATUS " AGAVE_VULKAN_SDK_ROOT: ${AGAVE_VULKAN_SDK_ROOT}") + message(STATUS " Vulkan header files in SDK: ${_agave_vulkan_header_candidates}") + message(STATUS " Vulkan loader files in SDK: ${_agave_vulkan_loader_candidates}") + message(STATUS " glslc files in SDK: ${_agave_vulkan_glslc_candidates}") + message(STATUS " shaderc files in SDK: ${_agave_vulkan_shaderc_candidates}") + + find_package(Vulkan COMPONENTS glslc shaderc_combined) + + if(TARGET Vulkan::shaderc_combined) + set(_agave_vulkan_shaderc_target_found TRUE) + else() + set(_agave_vulkan_shaderc_target_found FALSE) + endif() + message(STATUS "Vulkan diagnostics after find_package:") + message(STATUS " Vulkan_FOUND: ${Vulkan_FOUND}") + message(STATUS " Vulkan_VERSION: ${Vulkan_VERSION}") + message(STATUS " Vulkan_INCLUDE_DIR: ${Vulkan_INCLUDE_DIR}") + message(STATUS " Vulkan_INCLUDE_DIRS: ${Vulkan_INCLUDE_DIRS}") + message(STATUS " Vulkan_LIBRARY: ${Vulkan_LIBRARY}") + message(STATUS " Vulkan_LIBRARIES: ${Vulkan_LIBRARIES}") + message(STATUS " Vulkan_glslc_FOUND: ${Vulkan_glslc_FOUND}") + message(STATUS " Vulkan_GLSLC_EXECUTABLE: ${Vulkan_GLSLC_EXECUTABLE}") + message(STATUS " Vulkan_shaderc_combined_FOUND: ${Vulkan_shaderc_combined_FOUND}") + message(STATUS " Vulkan_shaderc_combined_LIBRARY: ${Vulkan_shaderc_combined_LIBRARY}") + message(STATUS " Vulkan::shaderc_combined target exists: ${_agave_vulkan_shaderc_target_found}") + + if(Vulkan_FOUND AND TARGET Vulkan::shaderc_combined) set(AGAVE_HAS_VULKAN ON) set(AGAVE_VULKAN_RUNTIME_LIBRARY "${Vulkan_LIBRARY}") if(APPLE) @@ -216,22 +216,22 @@ if(AGAVE_ENABLE_VULKAN) else() message(WARNING "libvulkan_kosmickrisp.dylib was not found; falling back to ${Vulkan_LIBRARY}") endif() - endif() - message(STATUS "Vulkan backend enabled: SDK ${Vulkan_VERSION}") - elseif(AGAVE_BUILD_PYVK) - message(FATAL_ERROR - "AGAVE_BUILD_PYVK requires Vulkan, glslc, and shaderc_combined. " - "See the Vulkan diagnostics above for the missing component." - ) - else() - message(WARNING "AGAVE_ENABLE_VULKAN is ON, but a required Vulkan component was not found. Vulkan backend disabled.") - endif() - unset(_agave_vulkan_glslc_candidates) - unset(_agave_vulkan_shaderc_candidates) - unset(_agave_vulkan_header_candidates) - unset(_agave_vulkan_loader_candidates) - unset(_agave_vulkan_shaderc_target_found) -endif() + endif() + message(STATUS "Vulkan backend enabled: SDK ${Vulkan_VERSION}") + elseif(AGAVE_BUILD_PYVK) + message(FATAL_ERROR + "AGAVE_BUILD_PYVK requires Vulkan, glslc, and shaderc_combined. " + "See the Vulkan diagnostics above for the missing component." + ) + else() + message(WARNING "AGAVE_ENABLE_VULKAN is ON, but a required Vulkan component was not found. Vulkan backend disabled.") + endif() + unset(_agave_vulkan_glslc_candidates) + unset(_agave_vulkan_shaderc_candidates) + unset(_agave_vulkan_header_candidates) + unset(_agave_vulkan_loader_candidates) + unset(_agave_vulkan_shaderc_target_found) +endif() # normally CMAKE_INSTALL_PREFIX is meant to be outside of the build tree if("${CMAKE_BUILD_TYPE}" STREQUAL "") @@ -249,7 +249,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # ##################### add_subdirectory(renderlib) -if(AGAVE_BUILD_PYVK) +if(AGAVE_BUILD_PYVK) add_subdirectory(agave_pyvk) endif() From 2f72735186e50fb34320577e073f05db226b32b9 Mon Sep 17 00:00:00 2001 From: toloudis Date: Mon, 20 Jul 2026 08:37:21 -0700 Subject: [PATCH 16/35] try latest and greatest vulkan sdk --- .github/workflows/build_and_test.yml | 5 ++++- .github/workflows/tagged_master_release.yml | 5 ++++- agave_pyvk/pyproject.toml | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index ce805241b9..d53232df77 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -87,11 +87,14 @@ jobs: include: - os: ubuntu-latest artifact: agave-linux + vulkan_version: 1.4.350.1 - os: windows-latest artifact: agave-win + vulkan_version: 1.4.350.0 - os: macos-15 artifact: agave-macos-arm64 deployment_target: "11.0" + vulkan_version: 1.4.350.1 runs-on: ${{ matrix.os }} @@ -111,7 +114,7 @@ jobs: - name: Install Vulkan SDK uses: hollykbuck/install-vulkan-sdk@c96510125668208b8cd5069cd667322b0eaedb3d with: - version: 1.4.350.0 + version: ${{ matrix.vulkan_version }} cache: true - name: Diagnose Unix Vulkan SDK installation if: matrix.os != 'windows-latest' diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index e81d2d59b8..65bf184848 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -17,11 +17,14 @@ jobs: include: - os: ubuntu-latest artifact: agave-linux + vulkan_version: 1.4.350.1 - os: windows-latest artifact: agave-win + vulkan_version: 1.4.350.0 - os: macos-15 artifact: agave-macos-arm64 deployment_target: "11.0" + vulkan_version: 1.4.350.1 runs-on: ${{ matrix.os }} @@ -41,7 +44,7 @@ jobs: - name: Install Vulkan SDK uses: hollykbuck/install-vulkan-sdk@c96510125668208b8cd5069cd667322b0eaedb3d with: - version: 1.4.350.0 + version: ${{ matrix.vulkan_version }} cache: true - name: Diagnose Unix Vulkan SDK installation if: matrix.os != 'windows-latest' diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index fa0038e691..6ad56292ae 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -38,13 +38,13 @@ environment = { AGAVE_PYVK_USE_PREBUILT = "1" } [tool.cibuildwheel.linux] manylinux-x86_64-image = "manylinux_2_35" -environment = { VULKAN_SDK = "/opt/1.4.350.0/x86_64", AGAVE_PYVK_CMAKE_BUILD_DIR = "/tmp/agave-pyvk-build", AGAVE_PYVK_PREBUILT_DIR = "/tmp/agave-pyvk-stage", AGAVE_PYVK_USE_PREBUILT = "1" } +environment = { VULKAN_SDK = "/opt/1.4.350.1/x86_64", AGAVE_PYVK_CMAKE_BUILD_DIR = "/tmp/agave-pyvk-build", AGAVE_PYVK_PREBUILT_DIR = "/tmp/agave-pyvk-stage", AGAVE_PYVK_USE_PREBUILT = "1" } before-all = [ "apt-get update", "apt-get install -y cmake curl git libcurl4-openssl-dev libegl1-mesa-dev libgl1-mesa-dev liblzma-dev libspdlog-dev libtiff-dev libx11-dev libxcb1-dev libzstd-dev nasm ninja-build patch pkg-config xz-utils zlib1g-dev", "python3 -m pip install 'cmake>=3.24' ninja", - "curl --fail --location https://sdk.lunarg.com/sdk/download/1.4.350.0/linux/vulkan_sdk.tar.xz --output /tmp/vulkan_sdk.tar.xz", - "echo 'b65f068ab36263559da49d7cacd7e7b9df23824ca8b68ccc522a2b06f5725df2 /tmp/vulkan_sdk.tar.xz' | sha256sum --check", + "curl --fail --location https://sdk.lunarg.com/sdk/download/1.4.350.1/linux/vulkan_sdk.tar.xz --output /tmp/vulkan_sdk.tar.xz", + "echo '6cce33c7e5383814150c5041820769d93c65a1fd883002e5949b067045a07daa /tmp/vulkan_sdk.tar.xz' | sha256sum --check", "mkdir -p /opt && tar -xf /tmp/vulkan_sdk.tar.xz -C /opt", "cmake -S /project -B /tmp/agave-pyvk-build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=OFF", "cmake --build /tmp/agave-pyvk-build --target renderlib --parallel", From 53d8f2ad99a62abad5df8a133b7b86f9fd2f041a Mon Sep 17 00:00:00 2001 From: toloudis Date: Mon, 20 Jul 2026 09:01:47 -0700 Subject: [PATCH 17/35] new vulkan sdk installer --- .github/workflows/build_and_test.yml | 4 ++-- .github/workflows/tagged_master_release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index d53232df77..d1b3cdf5fd 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -112,9 +112,9 @@ jobs: version: 6.9.3 modules: qtwebsockets qtimageformats - name: Install Vulkan SDK - uses: hollykbuck/install-vulkan-sdk@c96510125668208b8cd5069cd667322b0eaedb3d + uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa with: - version: ${{ matrix.vulkan_version }} + vulkan_version: ${{ matrix.vulkan_version }} cache: true - name: Diagnose Unix Vulkan SDK installation if: matrix.os != 'windows-latest' diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index 65bf184848..288a341c40 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -42,9 +42,9 @@ jobs: version: 6.9.3 modules: qtwebsockets qtimageformats - name: Install Vulkan SDK - uses: hollykbuck/install-vulkan-sdk@c96510125668208b8cd5069cd667322b0eaedb3d + uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa with: - version: ${{ matrix.vulkan_version }} + vulkan_version: ${{ matrix.vulkan_version }} cache: true - name: Diagnose Unix Vulkan SDK installation if: matrix.os != 'windows-latest' From 5c6716871c680d47129c2ac163fb790e4dae876a Mon Sep 17 00:00:00 2001 From: toloudis Date: Mon, 20 Jul 2026 11:18:36 -0700 Subject: [PATCH 18/35] rename font class --- renderlib/Font.cpp | 14 +++++++------- renderlib/Font.h | 6 +++--- renderlib/gesture/gesture.h | 2 +- renderlib/gfxOpenGL/FontGL.cpp | 4 ++-- renderlib/gfxOpenGL/FontGL.h | 2 +- renderlib/gfxVulkan/GestureRenderer.cpp | 2 +- renderlib/gfxVulkan/GestureRenderer.h | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/renderlib/Font.cpp b/renderlib/Font.cpp index 82a5b88c70..23e1902bac 100644 --- a/renderlib/Font.cpp +++ b/renderlib/Font.cpp @@ -7,15 +7,15 @@ static const int s_textureSize = 512; -Font::Font() {} +AgaveFont::AgaveFont() {} -Font::~Font() +AgaveFont::~AgaveFont() { unload(); } void -Font::load(const char* filename) +AgaveFont::load(const char* filename) { FILE* file = fopen(filename, "rb"); if (!file) { @@ -52,7 +52,7 @@ Font::load(const char* filename) } void -Font::unload() +AgaveFont::unload() { m_textureData.clear(); m_w = 0; @@ -60,7 +60,7 @@ Font::unload() } bool -Font::getBakedQuad(char char_index, float* x, float* y, stbtt_aligned_quad* q) +AgaveFont::getBakedQuad(char char_index, float* x, float* y, stbtt_aligned_quad* q) { if (char_index >= m_firstChar && char_index < (m_firstChar + m_numChars)) { stbtt_GetBakedQuad( @@ -81,7 +81,7 @@ Font::getBakedQuad(char char_index, float* x, float* y, stbtt_aligned_quad* q) } float -Font::getStringWidth(std::string stext) +AgaveFont::getStringWidth(std::string stext) { const char* text = stext.c_str(); float xpos = 0; @@ -100,7 +100,7 @@ Font::getStringWidth(std::string stext) } float -Font::getStringHeight(std::string stext) +AgaveFont::getStringHeight(std::string stext) { const char* text = stext.c_str(); float xpos = 0; diff --git a/renderlib/Font.h b/renderlib/Font.h index dbe149440d..1a18d6709b 100644 --- a/renderlib/Font.h +++ b/renderlib/Font.h @@ -6,11 +6,11 @@ #include #include -class Font +class AgaveFont { public: - Font(); - ~Font(); + AgaveFont(); + ~AgaveFont(); bool isLoaded() { return m_w > 0 && m_h > 0 && !m_textureData.empty(); } void load(const char* filename); diff --git a/renderlib/gesture/gesture.h b/renderlib/gesture/gesture.h index 28b4a41a4b..262bf1d7ed 100644 --- a/renderlib/gesture/gesture.h +++ b/renderlib/gesture/gesture.h @@ -278,7 +278,7 @@ struct Gesture // the thickness of each strip std::vector stripThicknesses; - Font font; + AgaveFont font; // remember selection code to reuse while dragging // 1 bit is reserved for component flags. diff --git a/renderlib/gfxOpenGL/FontGL.cpp b/renderlib/gfxOpenGL/FontGL.cpp index 3b62f49e17..9958ff4c93 100644 --- a/renderlib/gfxOpenGL/FontGL.cpp +++ b/renderlib/gfxOpenGL/FontGL.cpp @@ -5,7 +5,7 @@ #include GLuint -generateFontTexture(const Font& font) +generateFontTexture(const AgaveFont& font) { size_t w = font.getTextureWidth(); size_t h = font.getTextureHeight(); @@ -43,7 +43,7 @@ FontGL::~FontGL() } void -FontGL::load(const Font& font) +FontGL::load(const AgaveFont& font) { if (m_texID != 0) { return; diff --git a/renderlib/gfxOpenGL/FontGL.h b/renderlib/gfxOpenGL/FontGL.h index dcb98360a2..034a8b19f3 100644 --- a/renderlib/gfxOpenGL/FontGL.h +++ b/renderlib/gfxOpenGL/FontGL.h @@ -12,7 +12,7 @@ class FontGL FontGL(); ~FontGL(); - void load(const Font& font); + void load(const AgaveFont& font); void unload(); GLuint getTextureID() const { return m_texID; } diff --git a/renderlib/gfxVulkan/GestureRenderer.cpp b/renderlib/gfxVulkan/GestureRenderer.cpp index feca38a8d1..bbe41b2646 100644 --- a/renderlib/gfxVulkan/GestureRenderer.cpp +++ b/renderlib/gfxVulkan/GestureRenderer.cpp @@ -313,7 +313,7 @@ GestureRenderer::ensureCommonResources() } bool -GestureRenderer::ensureFontResources(const Font& font) +GestureRenderer::ensureFontResources(const AgaveFont& font) { // Nothing to do until the font atlas has been baked, or if we've already // uploaded it. Font atlas contents are baked once at load time and never diff --git a/renderlib/gfxVulkan/GestureRenderer.h b/renderlib/gfxVulkan/GestureRenderer.h index f304db3a00..afb2f9a502 100644 --- a/renderlib/gfxVulkan/GestureRenderer.h +++ b/renderlib/gfxVulkan/GestureRenderer.h @@ -14,7 +14,7 @@ #include #include -class Font; +class AgaveFont; namespace gfxApi { class Framebuffer; @@ -63,7 +63,7 @@ class GestureRenderer : public gfxApi::IGestureRenderer private: bool ensureBackend(); bool ensureCommonResources(); - bool ensureFontResources(const Font& font); + bool ensureFontResources(const AgaveFont& font); bool ensureSelectionFramebuffer(int width, int height); bool ensureDisplayPipelines(VkFormat colorFormat); bool ensureSelectionPipelines(); From b510801b1deec563b7b09e91d459965f6169fef1 Mon Sep 17 00:00:00 2001 From: toloudis Date: Mon, 20 Jul 2026 13:20:37 -0700 Subject: [PATCH 19/35] fix linux build by using QNativeInterface --- agave_app/QtVulkanSurface.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agave_app/QtVulkanSurface.cpp b/agave_app/QtVulkanSurface.cpp index 647ea2f96e..19855f9d79 100644 --- a/agave_app/QtVulkanSurface.cpp +++ b/agave_app/QtVulkanSurface.cpp @@ -7,7 +7,7 @@ #include #if defined(__linux__) -#include +#include #endif #include @@ -36,8 +36,8 @@ QtVulkanSurface::nativeDisplay() const // when presentation happens on a different xcb_connection_t than the one // that owns the window (some Mesa drivers keep per-connection present // state). - if (QPlatformNativeInterface* native = QGuiApplication::platformNativeInterface()) { - return native->nativeResourceForIntegration(QByteArrayLiteral("connection")); + if (auto* x11App = qGuiApp->nativeInterface()) { + return x11App->connection(); // returns xcb_connection_t* } return nullptr; #else From f187c9e458219f61276502520831cfd8294632f9 Mon Sep 17 00:00:00 2001 From: toloudis Date: Mon, 20 Jul 2026 15:07:59 -0700 Subject: [PATCH 20/35] fix include --- agave_app/QtVulkanSurface.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agave_app/QtVulkanSurface.cpp b/agave_app/QtVulkanSurface.cpp index 19855f9d79..51ffaf0d25 100644 --- a/agave_app/QtVulkanSurface.cpp +++ b/agave_app/QtVulkanSurface.cpp @@ -7,7 +7,8 @@ #include #if defined(__linux__) -#include +// For X11 / XCB platforms: +#include #endif #include From 9fc9939c375579307a2af1c722242ae652d45d13 Mon Sep 17 00:00:00 2001 From: toloudis Date: Mon, 20 Jul 2026 18:23:57 -0700 Subject: [PATCH 21/35] one more try at the right include --- agave_app/QtVulkanSurface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agave_app/QtVulkanSurface.cpp b/agave_app/QtVulkanSurface.cpp index 51ffaf0d25..ef357f35e7 100644 --- a/agave_app/QtVulkanSurface.cpp +++ b/agave_app/QtVulkanSurface.cpp @@ -7,8 +7,8 @@ #include #if defined(__linux__) -// For X11 / XCB platforms: -#include +// For X11 / XCB platforms: provides QNativeInterface::QX11Application +#include #endif #include From b8c41cdac8f8c191b7e4e2be2f6dd5565e4a1f22 Mon Sep 17 00:00:00 2001 From: toloudis Date: Tue, 21 Jul 2026 14:52:18 -0700 Subject: [PATCH 22/35] simplify build --- .github/workflows/build_and_test.yml | 10 +- .github/workflows/tagged_master_release.yml | 5 - .gitignore | 13 ++- CMakeLists.txt | 16 +-- README.md | 26 +++-- agave_pyvk/CMakeLists.txt | 17 ++++ agave_pyvk/README.md | 40 +++----- agave_pyvk/build_backend.py | 104 -------------------- agave_pyvk/pyproject.toml | 35 ++++--- agave_pyvk/setup.py | 9 ++ agave_pyvk/tools/build_native.py | 94 ------------------ 11 files changed, 90 insertions(+), 279 deletions(-) delete mode 100644 agave_pyvk/build_backend.py create mode 100644 agave_pyvk/setup.py delete mode 100644 agave_pyvk/tools/build_native.py diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index d1b3cdf5fd..2b93d8536d 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -255,15 +255,7 @@ jobs: path: ./build/${{ matrix.artifact }}.exe - name: Install Python package build frontend run: python -m pip install build - - name: Stage the agave_py2 CMake install component - env: - AGAVE_PYVK_CMAKE_BUILD_DIR: ${{ github.workspace }}/build - AGAVE_PYVK_PREBUILT_DIR: ${{ github.workspace }}/build/agave-pyvk-stage - run: python agave_pyvk/tools/build_native.py --stage-only - - name: Build agave_pyvk wheel from the staged CMake component - env: - AGAVE_PYVK_PREBUILT_DIR: ${{ github.workspace }}/build/agave-pyvk-stage - AGAVE_PYVK_USE_PREBUILT: "1" + - name: Build agave_pyvk wheel from the CMake-staged native module run: python -m build --wheel agave_pyvk - name: Upload agave_pyvk wheel uses: actions/upload-artifact@v6 diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index 288a341c40..e9f65b496a 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -189,16 +189,11 @@ jobs: - name: Build macOS agave_pyvk wheel matrix if: contains(matrix.os, 'macos') env: - AGAVE_PYVK_CMAKE_BUILD_DIR: ${{ github.workspace }}/build - AGAVE_PYVK_PREBUILT_DIR: ${{ github.workspace }}/build/agave-pyvk-stage MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target }} run: python -m cibuildwheel agave_pyvk --output-dir wheelhouse shell: bash - name: Build Windows agave_pyvk wheel matrix with MSVC 18 if: matrix.os == 'windows-latest' - env: - AGAVE_PYVK_CMAKE_BUILD_DIR: ${{ github.workspace }}\build - AGAVE_PYVK_PREBUILT_DIR: ${{ github.workspace }}\build\agave-pyvk-stage run: | call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 python -m cibuildwheel agave_pyvk --output-dir wheelhouse diff --git a/.gitignore b/.gitignore index 769963ef71..25ab87f03c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,17 @@ thirdparty/ webclient/node_modules/ webclient/es/ -build/ -renderlib/version.h +build/ +build-cibw-linux/ +**/__pycache__/ +**/*.egg-info/ +agave_pyvk/agave_pyvk/_native*.pyd +agave_pyvk/agave_pyvk/_native*.so +agave_pyvk/agave_pyvk/*.dll +agave_pyvk/agave_pyvk/*.dylib +agave_pyvk/agave_pyvk/*.so.* +agave_pyvk/agave_pyvk/assets/ +renderlib/version.h docs/_build/ GeneratedFiles/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d735a4ec6..71b8b3e145 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,10 +40,10 @@ project( LANGUAGES C CXX ) -option(AGAVE_BUILD_APP "Build the Qt desktop application and installers" ON) -option(AGAVE_BUILD_TESTS "Build the C++ test suite" ON) - -set(CMAKE_CXX_STANDARD 17) +option(AGAVE_BUILD_APP "Build the Qt desktop application and installers" ON) +option(AGAVE_BUILD_TESTS "Build the C++ test suite" ON) + +set(CMAKE_CXX_STANDARD 17) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) IF(WIN32) @@ -241,8 +241,8 @@ else() endif() message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # ##################### # THE COMMON CORE LIBRARIES @@ -258,8 +258,8 @@ set(INSTALLDIR "${CMAKE_PROJECT_NAME}-install") message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") message(STATUS "INSTALLDIR: ${INSTALLDIR}") message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}") -message(STATUS "CMAKE_LIBRARY_OUTPUT_DIRECTORY: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -message(STATUS "CMAKE_RUNTIME_OUTPUT_DIRECTORY: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") +message(STATUS "CMAKE_LIBRARY_OUTPUT_DIRECTORY: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +message(STATUS "CMAKE_RUNTIME_OUTPUT_DIRECTORY: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") # ##################### # UNIT TESTING diff --git a/README.md b/README.md index cb7c6d2de8..341d6ba1b7 100644 --- a/README.md +++ b/README.md @@ -112,29 +112,25 @@ If cmake fails please refer to the Dockerfile for a more complete list of Linux ### Iterative standalone Python development -The platform commands above create a persistent `build` directory containing -renderlib and the `agave_py2` nanobind target. After changing the Python binding -or renderlib, the following is the shortest build-and-install loop on macOS and -Linux: +The platform commands above create a persistent `build` directory. Its default +build compiles renderlib, the desktop application, and the `agave_py2` nanobind +module. Build there, then install the Python package into the active environment: ```console -python agave_pyvk/tools/build_native.py -AGAVE_PYVK_USE_PREBUILT=1 python -m pip install --force-reinstall --no-deps ./agave_pyvk +cmake --build . ``` -Run the equivalent commands from a VS2026 x64 Native Tools Command Prompt on -Windows: +Then, from the repository's `agave_pyvk` directory: ```console -python agave_pyvk\tools\build_native.py -set "AGAVE_PYVK_USE_PREBUILT=1" -python -m pip install --force-reinstall --no-deps .\agave_pyvk +python -m pip install -e . ``` -`build_native.py` reuses `build`, builds only targets that are out of date, and -stages the `agave_pyvk` install component. The environment variable tells pip -to package that staged native module instead of starting a separate CMake build. -The first install should omit `--no-deps` if NumPy and Pillow are not installed. +Run the same commands from a VS2026 x64 Native Tools Command Prompt on Windows. +The CMake build stages the native module, runtime dependencies, and assets +directly in the ignored portions of the Python package directory. The editable +install does not configure CMake or rebuild renderlib. Re-run `cmake --build .` +after native code changes; Python source changes are visible immediately. For a headless-only build tree, use the same platform-specific configure command with these options: diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt index 107d7e4dc7..07f785c3ab 100644 --- a/agave_pyvk/CMakeLists.txt +++ b/agave_pyvk/CMakeLists.txt @@ -13,6 +13,11 @@ endif() nanobind_add_module(agave_py2 NB_STATIC src/bindings.cpp src/VulkanRenderer.cpp) set_target_properties(agave_py2 PROPERTIES OUTPUT_NAME _native) +if(WIN32) + set_target_properties(agave_py2 PROPERTIES SUFFIX ".pyd") +else() + set_target_properties(agave_py2 PROPERTIES SUFFIX ".so") +endif() target_include_directories(agave_py2 PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/.." @@ -46,3 +51,15 @@ endif() install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../renderlib/assets/" DESTINATION agave_pyvk/assets COMPONENT agave_pyvk) + +set(AGAVE_PYVK_STAGE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +add_custom_target(stage_agave_pyvk ALL + COMMAND "${CMAKE_COMMAND}" + --install "${CMAKE_BINARY_DIR}" + --config "$" + --component agave_pyvk + --prefix "${AGAVE_PYVK_STAGE_DIR}" + DEPENDS agave_py2 + COMMENT "Staging agave_pyvk native package files" + VERBATIM +) diff --git a/agave_pyvk/README.md b/agave_pyvk/README.md index a627b19949..6ca8429aab 100644 --- a/agave_pyvk/README.md +++ b/agave_pyvk/README.md @@ -18,41 +18,33 @@ with AgaveRenderer(mode="pathtrace") as renderer: A Vulkan 1.3-capable driver and the Vulkan SDK are required to build. The remaining native dependencies are the same ones used by renderlib. -## Reusing a local CMake build +## Editable development install -From the repository root, configure a persistent headless build and compile the -`agave_py2` nanobind target together with renderlib: +Configure the normal top-level AGAVE build once. Then build renderlib, the AGAVE +application, and the `agave_py2` nanobind module from that build directory: ```console -cmake -S . -B build -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -cmake --build build --target agave_py2 --config Release --parallel +cmake --build . ``` -On Windows, add the vcpkg toolchain and Ninja Multi-Config arguments shown in -the root `README.md`, and run from a VS2026 x64 Native Tools Command Prompt. -The helper below reconfigures this CMake-owned tree for the active Python -interpreter; renderlib is rebuilt only when it is out of date. - -For iterative development, install the staged package directly instead of -creating a wheel file. On macOS and Linux: +The default build stages the native module, its runtime dependencies, and its +assets directly in the ignored portions of `agave_pyvk/agave_pyvk`. From the +repository's `agave_pyvk` directory, install the package without another native +build: ```console -python agave_pyvk/tools/build_native.py -AGAVE_PYVK_USE_PREBUILT=1 python -m pip install --force-reinstall --no-deps ./agave_pyvk +python -m pip install -e . ``` -Or from a VS2026 x64 Native Tools Command Prompt on Windows: - -```console -python agave_pyvk\tools\build_native.py -set "AGAVE_PYVK_USE_PREBUILT=1" -python -m pip install --force-reinstall --no-deps .\agave_pyvk -``` +This editable-install hook uses the staged native module and never invokes +CMake, so it does not rebuild renderlib. Re-run `cmake --build .` after native +code changes; Python source edits are visible immediately. On Windows, run the +CMake build from a VS2026 x64 Native Tools Command Prompt. -To produce a wheel file from the same staged build, install the `build` frontend, -set `AGAVE_PYVK_USE_PREBUILT=1` as above, and run: +To produce a wheel file from the same staged native build, install the `build` +frontend and run: ```console python -m pip install build -python -m build --wheel agave_pyvk +python -m build --wheel . ``` diff --git a/agave_pyvk/build_backend.py b/agave_pyvk/build_backend.py deleted file mode 100644 index 4d5b51dc28..0000000000 --- a/agave_pyvk/build_backend.py +++ /dev/null @@ -1,104 +0,0 @@ -"""PEP 517 backend supporting wheels staged by the shared CMake build.""" - -from __future__ import annotations - -import os -from email.message import Message -from pathlib import Path - -from packaging.tags import sys_tags -from scikit_build_core import build as _scikit_build -from wheel.wheelfile import WheelFile - -try: - import tomllib -except ModuleNotFoundError: # pragma: no cover - Python 3.10 - import tomli as tomllib - - -_PROJECT_ROOT = Path(__file__).resolve().parent - - -def _project_metadata() -> dict: - with (_PROJECT_ROOT / "pyproject.toml").open("rb") as stream: - return tomllib.load(stream)["project"] - - -def _metadata_text(project: dict) -> str: - metadata = Message() - metadata["Metadata-Version"] = "2.1" - metadata["Name"] = project["name"] - metadata["Version"] = project["version"] - metadata["Summary"] = project["description"] - metadata["Requires-Python"] = project["requires-python"] - for author in project.get("authors", []): - metadata["Author"] = author["name"] - metadata["Author-email"] = author["email"] - for dependency in project.get("dependencies", []): - metadata["Requires-Dist"] = dependency - metadata.set_payload("") - return metadata.as_string() - - -def _prebuilt_directory() -> Path: - configured = os.environ.get("AGAVE_PYVK_PREBUILT_DIR") - if configured: - return Path(configured).resolve() - return (_PROJECT_ROOT.parent / "build" / "agave-pyvk-stage").resolve() - - -def _build_prebuilt_wheel(wheel_directory: str) -> str: - package_stage = _prebuilt_directory() / "agave_pyvk" - if not package_stage.is_dir(): - raise RuntimeError(f"staged package directory does not exist: {package_stage}") - - current_tag = next(sys_tags()) - tag = f"{current_tag.interpreter}-{current_tag.abi}-{current_tag.platform}" - project = _project_metadata() - distribution = project["name"].replace("-", "_") - version = project["version"] - wheel_name = f"{distribution}-{version}-{tag}.whl" - dist_info = f"{distribution}-{version}.dist-info" - output = Path(wheel_directory).resolve() - output.mkdir(parents=True, exist_ok=True) - - package_files = {} - for source_root in (_PROJECT_ROOT / "agave_pyvk", package_stage): - for source in source_root.rglob("*"): - if source.is_file() and "__pycache__" not in source.parts: - archive_path = Path("agave_pyvk") / source.relative_to(source_root) - package_files[archive_path.as_posix()] = source - - wheel_metadata = ( - "Wheel-Version: 1.0\n" - "Generator: agave_pyvk prebuilt backend\n" - "Root-Is-Purelib: false\n" - f"Tag: {tag}\n" - ) - with WheelFile(output / wheel_name, "w") as wheel: - for archive_path, source in sorted(package_files.items()): - wheel.write(source, archive_path) - wheel.writestr(f"{dist_info}/METADATA", _metadata_text(project)) - wheel.writestr(f"{dist_info}/WHEEL", wheel_metadata) - return wheel_name - - -def build_wheel( - wheel_directory: str, - config_settings=None, - metadata_directory: str | None = None, -) -> str: - if os.environ.get("AGAVE_PYVK_USE_PREBUILT") == "1": - return _build_prebuilt_wheel(wheel_directory) - return _scikit_build.build_wheel( - wheel_directory, config_settings, metadata_directory - ) - - -build_sdist = _scikit_build.build_sdist -build_editable = _scikit_build.build_editable -get_requires_for_build_wheel = _scikit_build.get_requires_for_build_wheel -get_requires_for_build_sdist = _scikit_build.get_requires_for_build_sdist -get_requires_for_build_editable = _scikit_build.get_requires_for_build_editable -prepare_metadata_for_build_wheel = _scikit_build.prepare_metadata_for_build_wheel -prepare_metadata_for_build_editable = _scikit_build.prepare_metadata_for_build_editable diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index 6ad56292ae..b9c0ba9377 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -1,12 +1,9 @@ [build-system] requires = [ - "scikit-build-core>=0.10", - "packaging>=24", - "tomli>=2; python_version<'3.11'", + "setuptools>=69", "wheel>=0.43", ] -build-backend = "build_backend" -backend-path = ["."] +build-backend = "setuptools.build_meta" [project] name = "agave_pyvk" @@ -21,24 +18,22 @@ dependencies = ["numpy", "Pillow==12.2.0"] [project.optional-dependencies] test = ["pytest>=5.4.3", "ruff>=0.4"] -[tool.scikit-build] -cmake.source-dir = ".." -cmake.build-type = "Release" -cmake.define.AGAVE_BUILD_PYVK = "ON" -cmake.define.AGAVE_BUILD_APP = "OFF" -cmake.define.AGAVE_BUILD_TESTS = "OFF" -install.components = ["agave_pyvk"] -wheel.packages = ["agave_pyvk"] +[tool.setuptools] +packages = ["agave_pyvk"] +include-package-data = true + +[tool.setuptools.package-data] +agave_pyvk = ["*.pyd", "*.dll", "*.so", "*.so.*", "*.dylib", "assets/**/*"] [tool.cibuildwheel] build = "cp3{10,11,12,13,14}-*" skip = "*-musllinux_*" -before-build = "python {project}/agave_pyvk/tools/build_native.py" -environment = { AGAVE_PYVK_USE_PREBUILT = "1" } +archs = ["auto64"] [tool.cibuildwheel.linux] manylinux-x86_64-image = "manylinux_2_35" -environment = { VULKAN_SDK = "/opt/1.4.350.1/x86_64", AGAVE_PYVK_CMAKE_BUILD_DIR = "/tmp/agave-pyvk-build", AGAVE_PYVK_PREBUILT_DIR = "/tmp/agave-pyvk-stage", AGAVE_PYVK_USE_PREBUILT = "1" } +environment = { VULKAN_SDK = "/opt/1.4.350.1/x86_64" } +before-build = """cmake -S {package}/.. -B {package}/../build-cibw-linux -USKBUILD_* -UPython_* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build-cibw-linux --target stage_agave_pyvk --config Release --parallel""" before-all = [ "apt-get update", "apt-get install -y cmake curl git libcurl4-openssl-dev libegl1-mesa-dev libgl1-mesa-dev liblzma-dev libspdlog-dev libtiff-dev libx11-dev libxcb1-dev libzstd-dev nasm ninja-build patch pkg-config xz-utils zlib1g-dev", @@ -46,12 +41,16 @@ before-all = [ "curl --fail --location https://sdk.lunarg.com/sdk/download/1.4.350.1/linux/vulkan_sdk.tar.xz --output /tmp/vulkan_sdk.tar.xz", "echo '6cce33c7e5383814150c5041820769d93c65a1fd883002e5949b067045a07daa /tmp/vulkan_sdk.tar.xz' | sha256sum --check", "mkdir -p /opt && tar -xf /tmp/vulkan_sdk.tar.xz -C /opt", - "cmake -S /project -B /tmp/agave-pyvk-build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=OFF", - "cmake --build /tmp/agave-pyvk-build --target renderlib --parallel", + "cmake -S /project -B /project/build-cibw-linux -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=OFF", + "cmake --build /project/build-cibw-linux --target renderlib --parallel", ] [tool.cibuildwheel.macos] archs = ["arm64"] +before-build = """cmake -S {package}/.. -B {package}/../build -USKBUILD_* -UPython_* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel""" + +[tool.cibuildwheel.windows] +before-build = '''for /f "delims=" %P in ('python -c "import sys; print(sys.executable)"') do (cmake -S {package}/.. -B {package}/../build -USKBUILD_* -UPython_* -DPython_EXECUTABLE="%P" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel)''' [tool.ruff] line-length = 88 diff --git a/agave_pyvk/setup.py b/agave_pyvk/setup.py new file mode 100644 index 0000000000..51814a44bc --- /dev/null +++ b/agave_pyvk/setup.py @@ -0,0 +1,9 @@ +from setuptools import Distribution, setup + + +class BinaryDistribution(Distribution): + def has_ext_modules(self): + return True + + +setup(distclass=BinaryDistribution) diff --git a/agave_pyvk/tools/build_native.py b/agave_pyvk/tools/build_native.py deleted file mode 100644 index 71bcf98b19..0000000000 --- a/agave_pyvk/tools/build_native.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Build and stage agave_py2 using a persistent, CMake-owned build tree.""" - -from __future__ import annotations - -import argparse -import os -import shutil -import subprocess -import sys -from pathlib import Path - - -PROJECT_ROOT = Path(__file__).resolve().parents[2] - - -def _configured_path(name: str, default: Path) -> Path: - value = os.environ.get(name) - return Path(value).resolve() if value else default.resolve() - - -def _run(command: list[str]) -> None: - subprocess.run(command, check=True) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument( - "--stage-only", - action="store_true", - help="stage an agave_py2 target already built by CMake", - ) - args = parser.parse_args() - - if not args.stage_only and os.name == "nt" and "VSCMD_VER" not in os.environ: - raise RuntimeError( - "build_native.py must run from an MSVC x64 Native Tools prompt" - ) - - build_dir = _configured_path( - "AGAVE_PYVK_CMAKE_BUILD_DIR", PROJECT_ROOT / "build" - ) - stage_dir = _configured_path( - "AGAVE_PYVK_PREBUILT_DIR", build_dir / "agave-pyvk-stage" - ) - - if not args.stage_only: - _run( - [ - "cmake", - "-S", - str(PROJECT_ROOT), - "-B", - str(build_dir), - "-USKBUILD_*", - "-UPython_*", - "-DAGAVE_BUILD_APP=OFF", - "-DAGAVE_BUILD_TESTS=OFF", - "-DAGAVE_BUILD_PYVK=ON", - f"-DPython_EXECUTABLE={sys.executable}", - f"-DPython_ROOT_DIR={sys.prefix}", - ] - ) - _run( - [ - "cmake", - "--build", - str(build_dir), - "--target", - "agave_py2", - "--config", - "Release", - "--parallel", - ] - ) - - if stage_dir.exists(): - shutil.rmtree(stage_dir) - _run( - [ - "cmake", - "--install", - str(build_dir), - "--config", - "Release", - "--component", - "agave_pyvk", - "--prefix", - str(stage_dir), - ] - ) - - -if __name__ == "__main__": - main() From 42e298a7c13f6dc7a93cd86c1203d3ac53e9324b Mon Sep 17 00:00:00 2001 From: toloudis Date: Tue, 21 Jul 2026 15:28:46 -0700 Subject: [PATCH 23/35] more build tweaks --- .github/workflows/build_and_test.yml | 31 ++++---- .github/workflows/tagged_master_release.yml | 25 ++++++- .gitignore | 1 + CMakeLists.txt | 7 +- README.md | 21 +++--- agave_pyvk/CMakeLists.txt | 16 +++-- agave_pyvk/CleanStage.cmake | 10 +++ agave_pyvk/README.md | 21 +++--- agave_pyvk/agave_pyvk/__init__.py | 8 +++ agave_pyvk/build_backend.py | 79 +++++++++++++++++++++ agave_pyvk/cmake_build_dir.txt.in | 2 + agave_pyvk/pyproject.toml | 8 +-- 12 files changed, 179 insertions(+), 50 deletions(-) create mode 100644 agave_pyvk/CleanStage.cmake create mode 100644 agave_pyvk/build_backend.py create mode 100644 agave_pyvk/cmake_build_dir.txt.in diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 2b93d8536d..45aa2fcc8b 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -160,7 +160,7 @@ jobs: export Qt6_DIR=${{ runner.workspace }}/Qt/6.9.3/macos mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF cmake --build . --target agave_test --config Release cmake --build . --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -186,7 +186,7 @@ jobs: run: | mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF cmake --build . --target agave_test --config Release cmake --build . --target install --config Release shell: bash @@ -239,7 +239,7 @@ jobs: cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 if errorlevel 1 exit /b %errorlevel% - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF .. if errorlevel 1 exit /b %errorlevel% cmake --build . --parallel --target agave_test --config Release if errorlevel 1 exit /b %errorlevel% @@ -253,16 +253,21 @@ jobs: with: name: ${{ matrix.artifact }} path: ./build/${{ matrix.artifact }}.exe - - name: Install Python package build frontend - run: python -m pip install build - - name: Build agave_pyvk wheel from the CMake-staged native module - run: python -m build --wheel agave_pyvk - - name: Upload agave_pyvk wheel - uses: actions/upload-artifact@v6 - with: - name: agave-pyvk-${{ matrix.artifact }} - path: agave_pyvk/dist/*.whl - if-no-files-found: error + - name: Install agave_pyvk from the existing CMake build + if: matrix.os != 'windows-latest' + run: python -m pip install -e "agave_pyvk[test]" + - name: Install agave_pyvk from the existing CMake build with MSVC 18 + if: matrix.os == 'windows-latest' + run: | + call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 + if errorlevel 1 exit /b %errorlevel% + python -m pip install -e "agave_pyvk[test]" + if errorlevel 1 exit /b %errorlevel% + shell: cmd + - name: Test installed agave_pyvk package + run: | + python -c "from agave_pyvk import AgaveRenderer; import agave_pyvk._native" + python -m pytest agave_pyvk/tests lint-and-test-webclient: runs-on: ubuntu-latest diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index e9f65b496a..cf71bed9c3 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -90,7 +90,7 @@ jobs: export Qt6_DIR=${{ runner.workspace }}/Qt/6.9.3/macos mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF cmake --build . --target agave_test --config Release cmake --build . --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -117,7 +117,7 @@ jobs: pwd mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF cmake --build . --target agave_test --config Release cmake --build . --target install --config Release shell: bash @@ -170,7 +170,7 @@ jobs: mkdir build cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF .. cmake --build . --parallel --target agave_test --config Release cmake --build . --parallel --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -275,6 +275,24 @@ jobs: password: ${{ secrets.PYPI_TOKEN }} packages-dir: agave_pyclient/dist/ verbose: true + + publish_agave_pyvk: + needs: cmake-build + runs-on: ubuntu-latest + steps: + - name: Download agave_pyvk wheel artifacts + uses: actions/download-artifact@v4 + with: + pattern: agave-pyvk-* + path: agave_pyvk_wheelhouse + merge-multiple: true + - name: Publish agave_pyvk wheels to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + packages-dir: agave_pyvk_wheelhouse/ + verbose: true docs: needs: cmake-build runs-on: ubuntu-latest @@ -376,6 +394,7 @@ jobs: cmake-build, docs, docs_pythonclient, + publish_agave_pyvk, publish_pythonclient, publish_webclient, ] diff --git a/.gitignore b/.gitignore index 25ab87f03c..419a678f5e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build/ build-cibw-linux/ **/__pycache__/ **/*.egg-info/ +agave_pyvk/.cmake-build-dir agave_pyvk/agave_pyvk/_native*.pyd agave_pyvk/agave_pyvk/_native*.so agave_pyvk/agave_pyvk/*.dll diff --git a/CMakeLists.txt b/CMakeLists.txt index 71b8b3e145..b50de4ac22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,7 +94,12 @@ find_package(CURL REQUIRED) find_package(OpenGL) option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) -option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" ON) +option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" OFF) +configure_file( + "${CMAKE_SOURCE_DIR}/agave_pyvk/cmake_build_dir.txt.in" + "${CMAKE_SOURCE_DIR}/agave_pyvk/.cmake-build-dir" + @ONLY +) set(AGAVE_HAS_VULKAN OFF) set(AGAVE_VULKAN_RUNTIME_LIBRARY "") if(AGAVE_ENABLE_VULKAN) diff --git a/README.md b/README.md index 341d6ba1b7..28e6a15dd7 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ vcpkg install spdlog zlib libjpeg-turbo liblzma tiff zstd curl --triplet x64-win cmake -S . -B build -G "Ninja Multi-Config" ^ -DCMAKE_TOOLCHAIN_FILE=C:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake ^ -DVCPKG_TARGET_TRIPLET=x64-windows ^ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=OFF cmake --build build --config Release --parallel cmake --install build --config Release ``` @@ -80,7 +80,7 @@ export Qt6_DIR=~/Qt/6.9.3/macos brew install spdlog libtiff nasm curl cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=OFF cmake --build build --parallel cmake --install build ``` @@ -103,7 +103,7 @@ sudo apt install cmake ninja-build libtiff-dev libglm-dev libgl1-mesa-dev \ libzstd-dev zlib1g-dev nasm patch libxcb1-dev cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=OFF cmake --build build --parallel cmake --install build ``` @@ -113,8 +113,8 @@ If cmake fails please refer to the Dockerfile for a more complete list of Linux ### Iterative standalone Python development The platform commands above create a persistent `build` directory. Its default -build compiles renderlib, the desktop application, and the `agave_py2` nanobind -module. Build there, then install the Python package into the active environment: +build compiles renderlib and the desktop application. Build there, then install +the Python package into the active environment: ```console cmake --build . @@ -127,16 +127,17 @@ python -m pip install -e . ``` Run the same commands from a VS2026 x64 Native Tools Command Prompt on Windows. -The CMake build stages the native module, runtime dependencies, and assets -directly in the ignored portions of the Python package directory. The editable -install does not configure CMake or rebuild renderlib. Re-run `cmake --build .` -after native code changes; Python source changes are visible immediately. +The CMake configure records its build directory in the ignored +`agave_pyvk/.cmake-build-dir` marker. The editable install reads that marker, +configures the `agave_py2` target for the active Python interpreter, and links +it against renderlib in the existing build tree. It does not rebuild renderlib +unless renderlib is out of date. Python source changes are visible immediately. For a headless-only build tree, use the same platform-specific configure command with these options: ```console --DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON +-DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=OFF ``` ## Versioned Releases diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt index 07f785c3ab..0871a764c4 100644 --- a/agave_pyvk/CMakeLists.txt +++ b/agave_pyvk/CMakeLists.txt @@ -12,12 +12,10 @@ if(NOT AGAVE_HAS_VULKAN) endif() nanobind_add_module(agave_py2 NB_STATIC src/bindings.cpp src/VulkanRenderer.cpp) -set_target_properties(agave_py2 PROPERTIES OUTPUT_NAME _native) -if(WIN32) - set_target_properties(agave_py2 PROPERTIES SUFFIX ".pyd") -else() - set_target_properties(agave_py2 PROPERTIES SUFFIX ".so") -endif() +set_target_properties(agave_py2 PROPERTIES + OUTPUT_NAME _native + EXCLUDE_FROM_ALL TRUE +) target_include_directories(agave_py2 PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/.." @@ -53,7 +51,11 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../renderlib/assets/" COMPONENT agave_pyvk) set(AGAVE_PYVK_STAGE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") -add_custom_target(stage_agave_pyvk ALL +add_custom_target(stage_agave_pyvk + COMMAND "${CMAKE_COMMAND}" + "-DAGAVE_PYVK_PROJECT_DIR=${CMAKE_CURRENT_SOURCE_DIR}" + "-DAGAVE_PYVK_PACKAGE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/agave_pyvk" + -P "${CMAKE_CURRENT_SOURCE_DIR}/CleanStage.cmake" COMMAND "${CMAKE_COMMAND}" --install "${CMAKE_BINARY_DIR}" --config "$" diff --git a/agave_pyvk/CleanStage.cmake b/agave_pyvk/CleanStage.cmake new file mode 100644 index 0000000000..9c626a3072 --- /dev/null +++ b/agave_pyvk/CleanStage.cmake @@ -0,0 +1,10 @@ +if(NOT AGAVE_PYVK_PROJECT_DIR OR NOT AGAVE_PYVK_PACKAGE_DIR) + message(FATAL_ERROR "agave_pyvk stage directories were not provided") +endif() + +file(GLOB _agave_pyvk_native_modules + "${AGAVE_PYVK_PACKAGE_DIR}/_native*.pyd" + "${AGAVE_PYVK_PACKAGE_DIR}/_native*.so" +) +file(REMOVE ${_agave_pyvk_native_modules}) +file(REMOVE_RECURSE "${AGAVE_PYVK_PROJECT_DIR}/build") diff --git a/agave_pyvk/README.md b/agave_pyvk/README.md index 6ca8429aab..ac33832ada 100644 --- a/agave_pyvk/README.md +++ b/agave_pyvk/README.md @@ -20,26 +20,27 @@ remaining native dependencies are the same ones used by renderlib. ## Editable development install -Configure the normal top-level AGAVE build once. Then build renderlib, the AGAVE -application, and the `agave_py2` nanobind module from that build directory: +Configure the normal top-level AGAVE build once. Then build renderlib and the +AGAVE application from that build directory: ```console cmake --build . ``` -The default build stages the native module, its runtime dependencies, and its -assets directly in the ignored portions of `agave_pyvk/agave_pyvk`. From the -repository's `agave_pyvk` directory, install the package without another native -build: +The top-level configure records its build directory in the ignored +`agave_pyvk/.cmake-build-dir` marker. From the repository's `agave_pyvk` +directory, install the package for the active Python interpreter: ```console python -m pip install -e . ``` -This editable-install hook uses the staged native module and never invokes -CMake, so it does not rebuild renderlib. Re-run `cmake --build .` after native -code changes; Python source edits are visible immediately. On Windows, run the -CMake build from a VS2026 x64 Native Tools Command Prompt. +The pip backend reads the marker, reconfigures Python discovery for the active +interpreter, and builds only the `stage_agave_pyvk` target. That target links +against the existing renderlib CMake target, so renderlib is reused unless it is +out of date. Python source edits are visible immediately. On Windows, run pip +from a VS2026 x64 Native Tools Command Prompt so the compiler environment is +available. To produce a wheel file from the same staged native build, install the `build` frontend and run: diff --git a/agave_pyvk/agave_pyvk/__init__.py b/agave_pyvk/agave_pyvk/__init__.py index 3b995b59de..4e45a98e6d 100644 --- a/agave_pyvk/agave_pyvk/__init__.py +++ b/agave_pyvk/agave_pyvk/__init__.py @@ -1,5 +1,13 @@ """Headless in-process Vulkan bindings for AGAVE.""" +import os +from pathlib import Path + + +_dll_directory_handle = None +if os.name == "nt": + _dll_directory_handle = os.add_dll_directory(str(Path(__file__).resolve().parent)) + from .agave import AgaveRenderer __author__ = "Daniel Toloudis, Allen Institute" diff --git a/agave_pyvk/build_backend.py b/agave_pyvk/build_backend.py new file mode 100644 index 0000000000..9a85df3c3c --- /dev/null +++ b/agave_pyvk/build_backend.py @@ -0,0 +1,79 @@ +"""Setuptools backend that builds only the Python-specific CMake target.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +from setuptools import build_meta as _setuptools + + +_PROJECT_DIR = Path(__file__).resolve().parent +_SOURCE_DIR = _PROJECT_DIR.parent + + +def _cmake_settings() -> tuple[Path, str]: + marker = _PROJECT_DIR / ".cmake-build-dir" + if not marker.is_file(): + raise RuntimeError( + "No top-level AGAVE CMake build was found. Configure and build " + "AGAVE before installing agave_pyvk." + ) + + marker_lines = marker.read_text(encoding="utf-8").splitlines() + if not marker_lines or not marker_lines[0]: + raise RuntimeError(f"Invalid top-level CMake build marker: {marker}") + + build_dir = Path(marker_lines[0]).resolve() + build_type = marker_lines[1] if len(marker_lines) > 1 else "" + return build_dir, build_type + + +def _build_native() -> None: + build_dir, build_type = _cmake_settings() + subprocess.run( + [ + "cmake", + "-S", + str(_SOURCE_DIR), + "-B", + str(build_dir), + "-UPython_*", + "-DAGAVE_BUILD_PYVK=ON", + f"-DPython_EXECUTABLE={sys.executable}", + f"-DPython_ROOT_DIR={sys.prefix}", + ], + check=True, + ) + build_command = [ + "cmake", + "--build", + str(build_dir), + "--target", + "stage_agave_pyvk", + ] + if build_type: + build_command.extend(["--config", build_type]) + build_command.append("--parallel") + subprocess.run(build_command, check=True) + + +def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + _build_native() + return _setuptools.build_wheel(wheel_directory, config_settings, metadata_directory) + + +def build_editable(wheel_directory, config_settings=None, metadata_directory=None): + _build_native() + return _setuptools.build_editable( + wheel_directory, config_settings, metadata_directory + ) + + +build_sdist = _setuptools.build_sdist +get_requires_for_build_wheel = _setuptools.get_requires_for_build_wheel +get_requires_for_build_sdist = _setuptools.get_requires_for_build_sdist +get_requires_for_build_editable = _setuptools.get_requires_for_build_editable +prepare_metadata_for_build_wheel = _setuptools.prepare_metadata_for_build_wheel +prepare_metadata_for_build_editable = _setuptools.prepare_metadata_for_build_editable diff --git a/agave_pyvk/cmake_build_dir.txt.in b/agave_pyvk/cmake_build_dir.txt.in new file mode 100644 index 0000000000..7b3700dda0 --- /dev/null +++ b/agave_pyvk/cmake_build_dir.txt.in @@ -0,0 +1,2 @@ +@CMAKE_BINARY_DIR@ +@CMAKE_BUILD_TYPE@ diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index b9c0ba9377..740cef3d7b 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -3,7 +3,8 @@ requires = [ "setuptools>=69", "wheel>=0.43", ] -build-backend = "setuptools.build_meta" +build-backend = "build_backend" +backend-path = ["."] [project] name = "agave_pyvk" @@ -33,7 +34,6 @@ archs = ["auto64"] [tool.cibuildwheel.linux] manylinux-x86_64-image = "manylinux_2_35" environment = { VULKAN_SDK = "/opt/1.4.350.1/x86_64" } -before-build = """cmake -S {package}/.. -B {package}/../build-cibw-linux -USKBUILD_* -UPython_* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build-cibw-linux --target stage_agave_pyvk --config Release --parallel""" before-all = [ "apt-get update", "apt-get install -y cmake curl git libcurl4-openssl-dev libegl1-mesa-dev libgl1-mesa-dev liblzma-dev libspdlog-dev libtiff-dev libx11-dev libxcb1-dev libzstd-dev nasm ninja-build patch pkg-config xz-utils zlib1g-dev", @@ -47,10 +47,6 @@ before-all = [ [tool.cibuildwheel.macos] archs = ["arm64"] -before-build = """cmake -S {package}/.. -B {package}/../build -USKBUILD_* -UPython_* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel""" - -[tool.cibuildwheel.windows] -before-build = '''for /f "delims=" %P in ('python -c "import sys; print(sys.executable)"') do (cmake -S {package}/.. -B {package}/../build -USKBUILD_* -UPython_* -DPython_EXECUTABLE="%P" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel)''' [tool.ruff] line-length = 88 From 6b5f20071d0bacd089460dc2866c605f00282825 Mon Sep 17 00:00:00 2001 From: toloudis Date: Tue, 21 Jul 2026 17:06:50 -0700 Subject: [PATCH 24/35] simplify the python build again, assume it uses the same python interpreter that cmake did --- .github/workflows/build_and_test.yml | 6 +- .github/workflows/tagged_master_release.yml | 6 +- .gitignore | 1 - CMakeLists.txt | 7 +- README.md | 23 +++--- agave_pyvk/CMakeLists.txt | 3 +- agave_pyvk/README.md | 19 +++-- agave_pyvk/build_backend.py | 79 --------------------- agave_pyvk/cmake_build_dir.txt.in | 2 - agave_pyvk/pyproject.toml | 8 ++- 10 files changed, 33 insertions(+), 121 deletions(-) delete mode 100644 agave_pyvk/build_backend.py delete mode 100644 agave_pyvk/cmake_build_dir.txt.in diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 45aa2fcc8b..c4d2598f77 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -160,7 +160,7 @@ jobs: export Qt6_DIR=${{ runner.workspace }}/Qt/6.9.3/macos mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON cmake --build . --target agave_test --config Release cmake --build . --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -186,7 +186,7 @@ jobs: run: | mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON cmake --build . --target agave_test --config Release cmake --build . --target install --config Release shell: bash @@ -239,7 +239,7 @@ jobs: cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 if errorlevel 1 exit /b %errorlevel% - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF .. + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. if errorlevel 1 exit /b %errorlevel% cmake --build . --parallel --target agave_test --config Release if errorlevel 1 exit /b %errorlevel% diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index cf71bed9c3..d5e0c9bc4d 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -90,7 +90,7 @@ jobs: export Qt6_DIR=${{ runner.workspace }}/Qt/6.9.3/macos mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON cmake --build . --target agave_test --config Release cmake --build . --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} @@ -117,7 +117,7 @@ jobs: pwd mkdir ./build cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF + cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON cmake --build . --target agave_test --config Release cmake --build . --target install --config Release shell: bash @@ -170,7 +170,7 @@ jobs: mkdir build cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=OFF .. + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. cmake --build . --parallel --target agave_test --config Release cmake --build . --parallel --config Release cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} diff --git a/.gitignore b/.gitignore index 419a678f5e..25ab87f03c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ build/ build-cibw-linux/ **/__pycache__/ **/*.egg-info/ -agave_pyvk/.cmake-build-dir agave_pyvk/agave_pyvk/_native*.pyd agave_pyvk/agave_pyvk/_native*.so agave_pyvk/agave_pyvk/*.dll diff --git a/CMakeLists.txt b/CMakeLists.txt index b50de4ac22..1d31f2b97e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,12 +94,7 @@ find_package(CURL REQUIRED) find_package(OpenGL) option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) -option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" OFF) -configure_file( - "${CMAKE_SOURCE_DIR}/agave_pyvk/cmake_build_dir.txt.in" - "${CMAKE_SOURCE_DIR}/agave_pyvk/.cmake-build-dir" - @ONLY -) +option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" ON) set(AGAVE_HAS_VULKAN OFF) set(AGAVE_VULKAN_RUNTIME_LIBRARY "") if(AGAVE_ENABLE_VULKAN) diff --git a/README.md b/README.md index 28e6a15dd7..b3d7be196c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ vcpkg install spdlog zlib libjpeg-turbo liblzma tiff zstd curl --triplet x64-win cmake -S . -B build -G "Ninja Multi-Config" ^ -DCMAKE_TOOLCHAIN_FILE=C:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake ^ -DVCPKG_TARGET_TRIPLET=x64-windows ^ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=OFF + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON cmake --build build --config Release --parallel cmake --install build --config Release ``` @@ -80,7 +80,7 @@ export Qt6_DIR=~/Qt/6.9.3/macos brew install spdlog libtiff nasm curl cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=OFF + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON cmake --build build --parallel cmake --install build ``` @@ -103,7 +103,7 @@ sudo apt install cmake ninja-build libtiff-dev libglm-dev libgl1-mesa-dev \ libzstd-dev zlib1g-dev nasm patch libxcb1-dev cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=OFF + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON cmake --build build --parallel cmake --install build ``` @@ -112,9 +112,9 @@ If cmake fails please refer to the Dockerfile for a more complete list of Linux ### Iterative standalone Python development -The platform commands above create a persistent `build` directory. Its default -build compiles renderlib and the desktop application. Build there, then install -the Python package into the active environment: +The platform commands above create a persistent `build` directory. CMake uses +the active Python interpreter and its default build compiles renderlib, the +desktop application, and the ABI-specific `agave_py2` native module: ```console cmake --build . @@ -127,17 +127,16 @@ python -m pip install -e . ``` Run the same commands from a VS2026 x64 Native Tools Command Prompt on Windows. -The CMake configure records its build directory in the ignored -`agave_pyvk/.cmake-build-dir` marker. The editable install reads that marker, -configures the `agave_py2` target for the active Python interpreter, and links -it against renderlib in the existing build tree. It does not rebuild renderlib -unless renderlib is out of date. Python source changes are visible immediately. +The CMake build stages the native module and its runtime dependencies directly +in the Python package. The editable install performs only normal Python +packaging; it does not invoke CMake or rebuild renderlib. Python source changes +are visible immediately. For a headless-only build tree, use the same platform-specific configure command with these options: ```console --DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=OFF +-DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON ``` ## Versioned Releases diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt index 0871a764c4..ec3f5d64f0 100644 --- a/agave_pyvk/CMakeLists.txt +++ b/agave_pyvk/CMakeLists.txt @@ -14,7 +14,6 @@ endif() nanobind_add_module(agave_py2 NB_STATIC src/bindings.cpp src/VulkanRenderer.cpp) set_target_properties(agave_py2 PROPERTIES OUTPUT_NAME _native - EXCLUDE_FROM_ALL TRUE ) target_include_directories(agave_py2 PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" @@ -51,7 +50,7 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/../renderlib/assets/" COMPONENT agave_pyvk) set(AGAVE_PYVK_STAGE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") -add_custom_target(stage_agave_pyvk +add_custom_target(stage_agave_pyvk ALL COMMAND "${CMAKE_COMMAND}" "-DAGAVE_PYVK_PROJECT_DIR=${CMAKE_CURRENT_SOURCE_DIR}" "-DAGAVE_PYVK_PACKAGE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/agave_pyvk" diff --git a/agave_pyvk/README.md b/agave_pyvk/README.md index ac33832ada..c620f91c2e 100644 --- a/agave_pyvk/README.md +++ b/agave_pyvk/README.md @@ -20,27 +20,24 @@ remaining native dependencies are the same ones used by renderlib. ## Editable development install -Configure the normal top-level AGAVE build once. Then build renderlib and the -AGAVE application from that build directory: +Configure the normal top-level AGAVE build using the Python environment where +the package will be installed. Then build renderlib, the AGAVE application, and +the ABI-specific native module from that build directory: ```console cmake --build . ``` -The top-level configure records its build directory in the ignored -`agave_pyvk/.cmake-build-dir` marker. From the repository's `agave_pyvk` -directory, install the package for the active Python interpreter: +From the repository's `agave_pyvk` directory, install the package into that +same Python environment: ```console python -m pip install -e . ``` -The pip backend reads the marker, reconfigures Python discovery for the active -interpreter, and builds only the `stage_agave_pyvk` target. That target links -against the existing renderlib CMake target, so renderlib is reused unless it is -out of date. Python source edits are visible immediately. On Windows, run pip -from a VS2026 x64 Native Tools Command Prompt so the compiler environment is -available. +The CMake build stages `_native` and its runtime dependencies into the package +directory. Pip performs only normal Python packaging and does not invoke CMake. +Python source edits are visible immediately. To produce a wheel file from the same staged native build, install the `build` frontend and run: diff --git a/agave_pyvk/build_backend.py b/agave_pyvk/build_backend.py deleted file mode 100644 index 9a85df3c3c..0000000000 --- a/agave_pyvk/build_backend.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Setuptools backend that builds only the Python-specific CMake target.""" - -from __future__ import annotations - -import subprocess -import sys -from pathlib import Path - -from setuptools import build_meta as _setuptools - - -_PROJECT_DIR = Path(__file__).resolve().parent -_SOURCE_DIR = _PROJECT_DIR.parent - - -def _cmake_settings() -> tuple[Path, str]: - marker = _PROJECT_DIR / ".cmake-build-dir" - if not marker.is_file(): - raise RuntimeError( - "No top-level AGAVE CMake build was found. Configure and build " - "AGAVE before installing agave_pyvk." - ) - - marker_lines = marker.read_text(encoding="utf-8").splitlines() - if not marker_lines or not marker_lines[0]: - raise RuntimeError(f"Invalid top-level CMake build marker: {marker}") - - build_dir = Path(marker_lines[0]).resolve() - build_type = marker_lines[1] if len(marker_lines) > 1 else "" - return build_dir, build_type - - -def _build_native() -> None: - build_dir, build_type = _cmake_settings() - subprocess.run( - [ - "cmake", - "-S", - str(_SOURCE_DIR), - "-B", - str(build_dir), - "-UPython_*", - "-DAGAVE_BUILD_PYVK=ON", - f"-DPython_EXECUTABLE={sys.executable}", - f"-DPython_ROOT_DIR={sys.prefix}", - ], - check=True, - ) - build_command = [ - "cmake", - "--build", - str(build_dir), - "--target", - "stage_agave_pyvk", - ] - if build_type: - build_command.extend(["--config", build_type]) - build_command.append("--parallel") - subprocess.run(build_command, check=True) - - -def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): - _build_native() - return _setuptools.build_wheel(wheel_directory, config_settings, metadata_directory) - - -def build_editable(wheel_directory, config_settings=None, metadata_directory=None): - _build_native() - return _setuptools.build_editable( - wheel_directory, config_settings, metadata_directory - ) - - -build_sdist = _setuptools.build_sdist -get_requires_for_build_wheel = _setuptools.get_requires_for_build_wheel -get_requires_for_build_sdist = _setuptools.get_requires_for_build_sdist -get_requires_for_build_editable = _setuptools.get_requires_for_build_editable -prepare_metadata_for_build_wheel = _setuptools.prepare_metadata_for_build_wheel -prepare_metadata_for_build_editable = _setuptools.prepare_metadata_for_build_editable diff --git a/agave_pyvk/cmake_build_dir.txt.in b/agave_pyvk/cmake_build_dir.txt.in deleted file mode 100644 index 7b3700dda0..0000000000 --- a/agave_pyvk/cmake_build_dir.txt.in +++ /dev/null @@ -1,2 +0,0 @@ -@CMAKE_BINARY_DIR@ -@CMAKE_BUILD_TYPE@ diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index 740cef3d7b..a218e4e78c 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -3,8 +3,7 @@ requires = [ "setuptools>=69", "wheel>=0.43", ] -build-backend = "build_backend" -backend-path = ["."] +build-backend = "setuptools.build_meta" [project] name = "agave_pyvk" @@ -44,9 +43,14 @@ before-all = [ "cmake -S /project -B /project/build-cibw-linux -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=OFF", "cmake --build /project/build-cibw-linux --target renderlib --parallel", ] +before-build = """cmake -S {package}/.. -B {package}/../build-cibw-linux -USKBUILD_* -UPython_* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build-cibw-linux --target stage_agave_pyvk --config Release --parallel""" [tool.cibuildwheel.macos] archs = ["arm64"] +before-build = """cmake -S {package}/.. -B {package}/../build -USKBUILD_* -UPython_* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel""" + +[tool.cibuildwheel.windows] +before-build = '''for /f "delims=" %P in ('python -c "import sys; print(sys.executable)"') do (cmake -S {package}/.. -B {package}/../build -USKBUILD_* -UPython_* -DPython_EXECUTABLE="%P" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel)''' [tool.ruff] line-length = 88 From 2dd832a46a70c3e7513da2aa34a1d3306117d00f Mon Sep 17 00:00:00 2001 From: toloudis Date: Tue, 21 Jul 2026 17:27:23 -0700 Subject: [PATCH 25/35] cleanup --- agave_pyvk/CMakeLists.txt | 4 ++++ agave_pyvk/pyproject.toml | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt index ec3f5d64f0..e9b5553b18 100644 --- a/agave_pyvk/CMakeLists.txt +++ b/agave_pyvk/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.24) project(agave_pyvk VERSION 1.9.0 LANGUAGES CXX) find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) +# nanobind caches the ABI extension suffix. Recompute it when an existing CMake +# build tree is deliberately reconfigured for another Python interpreter. +unset(NB_SUFFIX CACHE) +unset(NB_SUFFIX_S CACHE) add_subdirectory(nanobind) if(NOT TARGET renderlib) diff --git a/agave_pyvk/pyproject.toml b/agave_pyvk/pyproject.toml index a218e4e78c..6f78d73af4 100644 --- a/agave_pyvk/pyproject.toml +++ b/agave_pyvk/pyproject.toml @@ -43,14 +43,14 @@ before-all = [ "cmake -S /project -B /project/build-cibw-linux -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=OFF", "cmake --build /project/build-cibw-linux --target renderlib --parallel", ] -before-build = """cmake -S {package}/.. -B {package}/../build-cibw-linux -USKBUILD_* -UPython_* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build-cibw-linux --target stage_agave_pyvk --config Release --parallel""" +before-build = """cmake -S {package}/.. -B {package}/../build-cibw-linux -USKBUILD_* -U_Python* -UPython_* -UNB_SUFFIX* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build-cibw-linux --target stage_agave_pyvk --config Release --parallel""" [tool.cibuildwheel.macos] archs = ["arm64"] -before-build = """cmake -S {package}/.. -B {package}/../build -USKBUILD_* -UPython_* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel""" +before-build = """cmake -S {package}/.. -B {package}/../build -USKBUILD_* -U_Python* -UPython_* -UNB_SUFFIX* -DPython_EXECUTABLE="$(python -c 'import sys; print(sys.executable)')" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel""" [tool.cibuildwheel.windows] -before-build = '''for /f "delims=" %P in ('python -c "import sys; print(sys.executable)"') do (cmake -S {package}/.. -B {package}/../build -USKBUILD_* -UPython_* -DPython_EXECUTABLE="%P" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel)''' +before-build = '''for /f "delims=" %P in ('python -c "import sys; print(sys.executable)"') do (cmake -S {package}/.. -B {package}/../build -USKBUILD_* -U_Python* -UPython_* -UNB_SUFFIX* -DPython_EXECUTABLE="%P" -DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON -DCMAKE_BUILD_TYPE=Release && cmake --build {package}/../build --target stage_agave_pyvk --config Release --parallel)''' [tool.ruff] line-length = 88 From 226072d0acd6c94f1527e63ef99c10762601382d Mon Sep 17 00:00:00 2001 From: toloudis Date: Tue, 21 Jul 2026 19:14:24 -0700 Subject: [PATCH 26/35] refactor gh action builds --- .github/workflows/build_and_test.yml | 249 +++++++---------- .github/workflows/tagged_master_release.yml | 288 +++++++++----------- 2 files changed, 234 insertions(+), 303 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index c4d2598f77..8f5ee6d935 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -80,192 +80,155 @@ jobs: with: name: DocumentationPythonClientPDF path: agave_pyclient/docs/_build/latex/agave_pyclient.pdf - cmake-build: - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - artifact: agave-linux - vulkan_version: 1.4.350.1 - - os: windows-latest - artifact: agave-win - vulkan_version: 1.4.350.0 - - os: macos-15 - artifact: agave-macos-arm64 - deployment_target: "11.0" - vulkan_version: 1.4.350.1 - - runs-on: ${{ matrix.os }} - + linux-build: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: submodules: "recursive" - - name: Set up Python - uses: actions/setup-python@v6 + - uses: actions/setup-python@v6 with: python-version: "3.12" - - name: Install Qt - uses: jurplel/install-qt-action@v4 + - uses: jurplel/install-qt-action@v4 with: version: 6.9.3 modules: qtwebsockets qtimageformats - - name: Install Vulkan SDK - uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa + - uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa with: - vulkan_version: ${{ matrix.vulkan_version }} + vulkan_version: 1.4.350.1 cache: true - - name: Diagnose Unix Vulkan SDK installation - if: matrix.os != 'windows-latest' + - name: Diagnose Vulkan SDK installation shell: bash run: | echo "VULKAN_SDK=$VULKAN_SDK" - echo "Vulkan header candidates:" - if [ -d "$VULKAN_SDK/include/vulkan" ]; then - find "$VULKAN_SDK/include/vulkan" -maxdepth 1 -name "vulkan.h" -print - else - echo "Missing $VULKAN_SDK/include/vulkan" - fi - echo "Vulkan loader and shaderc candidates:" - if [ -d "$VULKAN_SDK/lib" ]; then - find "$VULKAN_SDK/lib" -maxdepth 1 \( -name "libvulkan*" -o -name "*shaderc*" \) -print - else - echo "Missing $VULKAN_SDK/lib" - fi - echo "glslc candidates:" - if [ -d "$VULKAN_SDK/bin" ]; then - find "$VULKAN_SDK/bin" -maxdepth 1 -name "glslc*" -print - else - echo "Missing $VULKAN_SDK/bin" - fi - - name: macos install deps - if: contains(matrix.os, 'macos') + find "$VULKAN_SDK/include/vulkan" -maxdepth 1 -name "vulkan.h" -print + find "$VULKAN_SDK/lib" -maxdepth 1 \( -name "libvulkan*" -o -name "*shaderc*" \) -print + find "$VULKAN_SDK/bin" -maxdepth 1 -name "glslc*" -print + - name: Install dependencies + run: sudo apt-get install libspdlog-dev libglm-dev libgl1-mesa-dev libegl1-mesa-dev libtiff-dev libzstd-dev libcurl4-openssl-dev nasm + - name: Build and test env: - CC: clang - CXX: clang++ + CC: gcc-13 + CXX: g++-13 + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake --build build --target agave_test --config Release + cmake --build build --target install --config Release + - uses: actions/upload-artifact@v6 + with: + name: agave-linux + path: ./build/install/Release/agave-install + - name: Install and test agave_pyvk + run: | + python -m pip install -e "agave_pyvk[test]" + python -c "from agave_pyvk import AgaveRenderer; import agave_pyvk._native" + python -m pytest agave_pyvk/tests + + macos-build: + runs-on: macos-15 + env: + CC: clang + CXX: clang++ + MACOSX_DEPLOYMENT_TARGET: "11.0" + steps: + - uses: actions/checkout@v6 + with: + submodules: "recursive" + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - uses: jurplel/install-qt-action@v4 + with: + version: 6.9.3 + modules: qtwebsockets qtimageformats + - uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa + with: + vulkan_version: 1.4.350.1 + cache: true + - name: Diagnose Vulkan SDK installation + run: | + echo "VULKAN_SDK=$VULKAN_SDK" + find "$VULKAN_SDK/include/vulkan" -maxdepth 1 -name "vulkan.h" -print + find "$VULKAN_SDK/lib" -maxdepth 1 \( -name "libvulkan*" -o -name "*shaderc*" \) -print + find "$VULKAN_SDK/bin" -maxdepth 1 -name "glslc*" -print + - name: Install dependencies run: | - export MACOSX_DEPLOYMENT_TARGET=${{ matrix.deployment_target }} brew install spdlog nasm curl if [ -d /usr/local/include/openssl ]; then mv /usr/local/include/openssl /usr/local/include/openssl.bak fi - - name: macos build and test - if: contains(matrix.os, 'macos') + - name: Build and test env: - CC: clang - CXX: clang++ + Qt6_DIR: ${{ runner.workspace }}/Qt/6.9.3/macos run: | - export MACOSX_DEPLOYMENT_TARGET=${{ matrix.deployment_target }} - export Qt6_DIR=${{ runner.workspace }}/Qt/6.9.3/macos - mkdir ./build - cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON - cmake --build . --target agave_test --config Release - cmake --build . --config Release - cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} - shell: bash - - name: Upload mac artifact - if: contains(matrix.os, 'macos') - uses: actions/upload-artifact@v6 + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake --build build --target agave_test --config Release + cmake --build build --config Release + cpack --config build/CPackConfig.cmake -B build -D CPACK_PACKAGE_FILE_NAME=agave-macos-arm64 + - uses: actions/upload-artifact@v6 with: - name: ${{ matrix.artifact }} - path: ./build/${{ matrix.artifact }}.dmg - - name: linux install deps - if: matrix.os == 'ubuntu-latest' - env: - CC: gcc - CXX: g++ - run: | - sudo apt-get install libspdlog-dev libglm-dev libgl1-mesa-dev libegl1-mesa-dev libtiff-dev libzstd-dev libcurl4-openssl-dev nasm - - name: linux build and test - if: matrix.os == 'ubuntu-latest' - env: - CC: gcc-13 - CXX: g++-13 + name: agave-macos-arm64 + path: ./build/agave-macos-arm64.dmg + - name: Install and test agave_pyvk run: | - mkdir ./build - cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON - cmake --build . --target agave_test --config Release - cmake --build . --target install --config Release - shell: bash - - name: linux upload artifact - if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v6 + python -m pip install -e "agave_pyvk[test]" + python -c "from agave_pyvk import AgaveRenderer; import agave_pyvk._native" + python -m pytest agave_pyvk/tests + + windows-build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 with: - name: ${{ matrix.artifact }} - path: ./build/install/Release/agave-install - - name: windows install nasm - if: matrix.os == 'windows-latest' - run: | - choco install nasm - - name: windows install nsis - if: matrix.os == 'windows-latest' - run: choco install nsis -y - - name: windows install deps - if: matrix.os == 'windows-latest' + submodules: "recursive" + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - uses: jurplel/install-qt-action@v4 + with: + version: 6.9.3 + modules: qtwebsockets qtimageformats + - uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa + with: + vulkan_version: 1.4.350.0 + cache: true + - name: Install dependencies shell: cmd - env: - vc_arch: "x64" run: | + choco install nasm nsis -y call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 vcpkg install --triplet x64-windows tiff glm zlib libjpeg-turbo liblzma spdlog zstd curl - name: Remove bad Strawberry Perl patch binary in search path - # https://github.com/actions/runner-images/issues/5459#issuecomment-1532856844 - if: matrix.os == 'windows-latest' - run: | - echo "C:\Program Files\git\usr\bin" >> $env:GITHUB_PATH - - name: Diagnose Windows Vulkan SDK installation - if: matrix.os == 'windows-latest' + run: echo "C:\Program Files\git\usr\bin" >> $env:GITHUB_PATH + - name: Diagnose Vulkan SDK installation shell: pwsh run: | Write-Host "VULKAN_SDK=$env:VULKAN_SDK" - Write-Host "Vulkan header candidates:" - Get-Item -Path "$env:VULKAN_SDK\Include\vulkan\vulkan.h" -ErrorAction Continue - Write-Host "Vulkan loader candidates:" - Get-ChildItem -Path "$env:VULKAN_SDK\Lib" -Filter "vulkan*" -ErrorAction Continue - Write-Host "glslc candidates:" - Get-ChildItem -Path "$env:VULKAN_SDK\Bin" -Filter "glslc*" -ErrorAction Continue - Write-Host "shaderc candidates:" - Get-ChildItem -Path "$env:VULKAN_SDK\Lib" -Filter "*shaderc*" -ErrorAction Continue - - name: windows build and test - if: matrix.os == 'windows-latest' - env: - vc_arch: "x64" - # use older sdk to work around https://github.com/grpc/grpc/issues/37210 + Get-Item "$env:VULKAN_SDK\Include\vulkan\vulkan.h" -ErrorAction Continue + Get-ChildItem "$env:VULKAN_SDK\Lib" -Filter "vulkan*" -ErrorAction Continue + Get-ChildItem "$env:VULKAN_SDK\Bin" -Filter "glslc*" -ErrorAction Continue + Get-ChildItem "$env:VULKAN_SDK\Lib" -Filter "*shaderc*" -ErrorAction Continue + - name: Build and test + shell: cmd run: | - mkdir build - cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 + cmake -S . -B build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON if errorlevel 1 exit /b %errorlevel% - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. - if errorlevel 1 exit /b %errorlevel% - cmake --build . --parallel --target agave_test --config Release + cmake --build build --parallel --target agave_test --config Release if errorlevel 1 exit /b %errorlevel% - cmake --build . --parallel --config Release + cmake --build build --parallel --config Release if errorlevel 1 exit /b %errorlevel% - cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} - shell: cmd - - name: Upload windows artifact - if: matrix.os == 'windows-latest' - uses: actions/upload-artifact@v6 + cpack --config build\CPackConfig.cmake -B build -D CPACK_PACKAGE_FILE_NAME=agave-win + - uses: actions/upload-artifact@v6 with: - name: ${{ matrix.artifact }} - path: ./build/${{ matrix.artifact }}.exe - - name: Install agave_pyvk from the existing CMake build - if: matrix.os != 'windows-latest' - run: python -m pip install -e "agave_pyvk[test]" - - name: Install agave_pyvk from the existing CMake build with MSVC 18 - if: matrix.os == 'windows-latest' + name: agave-win + path: ./build/agave-win.exe + - name: Install and test agave_pyvk + shell: cmd run: | call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - if errorlevel 1 exit /b %errorlevel% python -m pip install -e "agave_pyvk[test]" if errorlevel 1 exit /b %errorlevel% - shell: cmd - - name: Test installed agave_pyvk package - run: | python -c "from agave_pyvk import AgaveRenderer; import agave_pyvk._native" python -m pytest agave_pyvk/tests diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index d5e0c9bc4d..ccb01cd2e7 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -11,202 +11,168 @@ permissions: contents: read jobs: - cmake-build: - strategy: - matrix: - include: - - os: ubuntu-latest - artifact: agave-linux - vulkan_version: 1.4.350.1 - - os: windows-latest - artifact: agave-win - vulkan_version: 1.4.350.0 - - os: macos-15 - artifact: agave-macos-arm64 - deployment_target: "11.0" - vulkan_version: 1.4.350.1 - - runs-on: ${{ matrix.os }} - + linux-build: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: submodules: "recursive" - - name: Set up Python - uses: actions/setup-python@v6 + - uses: actions/setup-python@v6 with: python-version: "3.12" - - name: Install Qt - uses: jurplel/install-qt-action@v4 + - uses: jurplel/install-qt-action@v4 with: version: 6.9.3 modules: qtwebsockets qtimageformats - - name: Install Vulkan SDK - uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa + - uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa with: - vulkan_version: ${{ matrix.vulkan_version }} + vulkan_version: 1.4.350.1 cache: true - - name: Diagnose Unix Vulkan SDK installation - if: matrix.os != 'windows-latest' - shell: bash + - name: Diagnose Vulkan SDK installation run: | echo "VULKAN_SDK=$VULKAN_SDK" - echo "Vulkan header candidates:" - if [ -d "$VULKAN_SDK/include/vulkan" ]; then - find "$VULKAN_SDK/include/vulkan" -maxdepth 1 -name "vulkan.h" -print - else - echo "Missing $VULKAN_SDK/include/vulkan" - fi - echo "Vulkan loader and shaderc candidates:" - if [ -d "$VULKAN_SDK/lib" ]; then - find "$VULKAN_SDK/lib" -maxdepth 1 \( -name "libvulkan*" -o -name "*shaderc*" \) -print - else - echo "Missing $VULKAN_SDK/lib" - fi - echo "glslc candidates:" - if [ -d "$VULKAN_SDK/bin" ]; then - find "$VULKAN_SDK/bin" -maxdepth 1 -name "glslc*" -print - else - echo "Missing $VULKAN_SDK/bin" - fi - - name: macos install deps - if: contains(matrix.os, 'macos') + find "$VULKAN_SDK/include/vulkan" -maxdepth 1 -name "vulkan.h" -print + find "$VULKAN_SDK/lib" -maxdepth 1 \( -name "libvulkan*" -o -name "*shaderc*" \) -print + find "$VULKAN_SDK/bin" -maxdepth 1 -name "glslc*" -print + - name: Install dependencies + run: sudo apt-get install libspdlog-dev libglm-dev libgl1-mesa-dev libegl1-mesa-dev libtiff-dev libzstd-dev libcurl4-openssl-dev nasm + - name: Build and test env: - CC: clang - CXX: clang++ + CC: gcc-13 + CXX: g++-13 + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake --build build --target agave_test --config Release + cmake --build build --target install --config Release + - uses: actions/upload-artifact@v6 + with: + name: agave-linux + path: ./build/install/Release/agave-install + - run: python -m pip install cibuildwheel==3.3.0 + - name: Build agave_pyvk wheels + run: python -m cibuildwheel agave_pyvk --output-dir wheelhouse + - uses: actions/upload-artifact@v6 + with: + name: agave-pyvk-agave-linux + path: wheelhouse/*.whl + if-no-files-found: error + + macos-build: + runs-on: macos-15 + env: + CC: clang + CXX: clang++ + MACOSX_DEPLOYMENT_TARGET: "11.0" + steps: + - uses: actions/checkout@v6 + with: + submodules: "recursive" + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - uses: jurplel/install-qt-action@v4 + with: + version: 6.9.3 + modules: qtwebsockets qtimageformats + - uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa + with: + vulkan_version: 1.4.350.1 + cache: true + - name: Diagnose Vulkan SDK installation + run: | + echo "VULKAN_SDK=$VULKAN_SDK" + find "$VULKAN_SDK/include/vulkan" -maxdepth 1 -name "vulkan.h" -print + find "$VULKAN_SDK/lib" -maxdepth 1 \( -name "libvulkan*" -o -name "*shaderc*" \) -print + find "$VULKAN_SDK/bin" -maxdepth 1 -name "glslc*" -print + - name: Install dependencies run: | - export MACOSX_DEPLOYMENT_TARGET=${{ matrix.deployment_target }} brew install spdlog nasm curl if [ -d /usr/local/include/openssl ]; then mv /usr/local/include/openssl /usr/local/include/openssl.bak fi - - name: macos build and test - if: contains(matrix.os, 'macos') - env: - CC: clang - CXX: clang++ - run: | - export MACOSX_DEPLOYMENT_TARGET=${{ matrix.deployment_target }} - export Qt6_DIR=${{ runner.workspace }}/Qt/6.9.3/macos - mkdir ./build - cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON - cmake --build . --target agave_test --config Release - cmake --build . --config Release - cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} - shell: bash - - name: Upload mac artifact - if: contains(matrix.os, 'macos') - uses: actions/upload-artifact@v6 - with: - name: ${{ matrix.artifact }} - path: ./build/${{ matrix.artifact }}.dmg - - name: linux install deps - if: matrix.os == 'ubuntu-latest' + - name: Build and test env: - CC: gcc - CXX: g++ + Qt6_DIR: ${{ runner.workspace }}/Qt/6.9.3/macos run: | - sudo apt-get install libspdlog-dev libglm-dev libgl1-mesa-dev libegl1-mesa-dev libtiff-dev libzstd-dev libcurl4-openssl-dev nasm - - name: linux build and test - if: matrix.os == 'ubuntu-latest' - env: - CC: gcc-13 - CXX: g++-13 - run: | - pwd - mkdir ./build - cd build - cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON - cmake --build . --target agave_test --config Release - cmake --build . --target install --config Release - shell: bash - - name: linux upload artifact - if: matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v6 + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake --build build --target agave_test --config Release + cmake --build build --config Release + cpack --config build/CPackConfig.cmake -B build -D CPACK_PACKAGE_FILE_NAME=agave-macos-arm64 + - uses: actions/upload-artifact@v6 with: - name: ${{ matrix.artifact }} - path: ./build/install/Release/agave-install - - name: windows install nasm - if: matrix.os == 'windows-latest' - run: | - choco install nasm - - name: windows install nsis - if: matrix.os == 'windows-latest' - run: choco install nsis -y - - name: windows install deps - if: matrix.os == 'windows-latest' + name: agave-macos-arm64 + path: ./build/agave-macos-arm64.dmg + - run: python -m pip install cibuildwheel==3.3.0 + - name: Build agave_pyvk wheels + run: python -m cibuildwheel agave_pyvk --output-dir wheelhouse + - uses: actions/upload-artifact@v6 + with: + name: agave-pyvk-agave-macos-arm64 + path: wheelhouse/*.whl + if-no-files-found: error + + windows-build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: "recursive" + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - uses: jurplel/install-qt-action@v4 + with: + version: 6.9.3 + modules: qtwebsockets qtimageformats + - uses: jakoch/install-vulkan-sdk-action@37effcfa045411f8bfbbda26df2fd1b3bf3436fa + with: + vulkan_version: 1.4.350.0 + cache: true + - name: Install dependencies shell: cmd - env: - vc_arch: "x64" - # use older sdk to work around https://github.com/grpc/grpc/issues/37210 run: | + choco install nasm nsis -y call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 vcpkg install --triplet x64-windows tiff glm zlib libjpeg-turbo liblzma spdlog zstd curl - name: Remove bad Strawberry Perl patch binary in search path - # https://github.com/actions/runner-images/issues/5459#issuecomment-1532856844 - if: matrix.os == 'windows-latest' - run: | - echo "C:\Program Files\git\usr\bin" >> $env:GITHUB_PATH - - name: Diagnose Windows Vulkan SDK installation - if: matrix.os == 'windows-latest' + run: echo "C:\Program Files\git\usr\bin" >> $env:GITHUB_PATH + - name: Diagnose Vulkan SDK installation shell: pwsh run: | Write-Host "VULKAN_SDK=$env:VULKAN_SDK" - Write-Host "Vulkan header candidates:" - Get-Item -Path "$env:VULKAN_SDK\Include\vulkan\vulkan.h" -ErrorAction Continue - Write-Host "Vulkan loader candidates:" - Get-ChildItem -Path "$env:VULKAN_SDK\Lib" -Filter "vulkan*" -ErrorAction Continue - Write-Host "glslc candidates:" - Get-ChildItem -Path "$env:VULKAN_SDK\Bin" -Filter "glslc*" -ErrorAction Continue - Write-Host "shaderc candidates:" - Get-ChildItem -Path "$env:VULKAN_SDK\Lib" -Filter "*shaderc*" -ErrorAction Continue - - name: windows build and test - if: matrix.os == 'windows-latest' - env: - vc_arch: "x64" - # use older sdk to work around https://github.com/grpc/grpc/issues/37210 + Get-Item "$env:VULKAN_SDK\Include\vulkan\vulkan.h" -ErrorAction Continue + Get-ChildItem "$env:VULKAN_SDK\Lib" -Filter "vulkan*" -ErrorAction Continue + Get-ChildItem "$env:VULKAN_SDK\Bin" -Filter "glslc*" -ErrorAction Continue + Get-ChildItem "$env:VULKAN_SDK\Lib" -Filter "*shaderc*" -ErrorAction Continue + - name: Build and test + shell: cmd run: | - mkdir build - cd build call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON .. - cmake --build . --parallel --target agave_test --config Release - cmake --build . --parallel --config Release - cpack -D CPACK_PACKAGE_FILE_NAME=${{ matrix.artifact }} + cmake -S . -B build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + if errorlevel 1 exit /b %errorlevel% + cmake --build build --parallel --target agave_test --config Release + if errorlevel 1 exit /b %errorlevel% + cmake --build build --parallel --config Release + if errorlevel 1 exit /b %errorlevel% + cpack --config build\CPackConfig.cmake -B build -D CPACK_PACKAGE_FILE_NAME=agave-win + - uses: actions/upload-artifact@v6 + with: + name: agave-win + path: ./build/agave-win.exe + - run: python -m pip install cibuildwheel==3.3.0 + - name: Build agave_pyvk wheels with MSVC 18 shell: cmd - - name: Upload windows artifact - if: matrix.os == 'windows-latest' - uses: actions/upload-artifact@v6 - with: - name: ${{ matrix.artifact }} - path: ./build/${{ matrix.artifact }}.exe - - name: Install cibuildwheel - run: python -m pip install cibuildwheel==3.3.0 - - name: Build Linux agave_pyvk wheel matrix - if: matrix.os == 'ubuntu-latest' - run: python -m cibuildwheel agave_pyvk --output-dir wheelhouse - - name: Build macOS agave_pyvk wheel matrix - if: contains(matrix.os, 'macos') - env: - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target }} - run: python -m cibuildwheel agave_pyvk --output-dir wheelhouse - shell: bash - - name: Build Windows agave_pyvk wheel matrix with MSVC 18 - if: matrix.os == 'windows-latest' run: | call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 python -m cibuildwheel agave_pyvk --output-dir wheelhouse - shell: cmd - - name: Upload agave_pyvk wheels - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v6 with: - name: agave-pyvk-${{ matrix.artifact }} + name: agave-pyvk-agave-win path: wheelhouse/*.whl if-no-files-found: error publish_webclient: - needs: cmake-build + needs: [linux-build, macos-build, windows-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -249,7 +215,7 @@ jobs: npm publish --provenance --tag ${{ steps.extract_version.outputs.npm_tag }} publish_pythonclient: - needs: cmake-build + needs: [linux-build, macos-build, windows-build] runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -277,7 +243,7 @@ jobs: verbose: true publish_agave_pyvk: - needs: cmake-build + needs: [linux-build, macos-build, windows-build] runs-on: ubuntu-latest steps: - name: Download agave_pyvk wheel artifacts @@ -294,7 +260,7 @@ jobs: packages-dir: agave_pyvk_wheelhouse/ verbose: true docs: - needs: cmake-build + needs: [linux-build, macos-build, windows-build] runs-on: ubuntu-latest permissions: contents: write @@ -340,7 +306,7 @@ jobs: folder: docs/_build/html/ # The folder the action should deploy. clean: false docs_pythonclient: - needs: [cmake-build, docs] + needs: [linux-build, macos-build, windows-build, docs] runs-on: ubuntu-latest permissions: contents: write @@ -391,7 +357,9 @@ jobs: make-release: needs: [ - cmake-build, + linux-build, + macos-build, + windows-build, docs, docs_pythonclient, publish_agave_pyvk, From 964e001563e62fe353a145d7d48f3244ceb40153 Mon Sep 17 00:00:00 2001 From: toloudis Date: Tue, 21 Jul 2026 19:10:24 -0700 Subject: [PATCH 27/35] fix apple packaging of python agave_pyvk --- agave_pyvk/CMakeLists.txt | 19 +++++++++++++++++++ agave_pyvk/CleanStage.cmake | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt index e9b5553b18..a04892f85a 100644 --- a/agave_pyvk/CMakeLists.txt +++ b/agave_pyvk/CMakeLists.txt @@ -19,6 +19,14 @@ nanobind_add_module(agave_py2 NB_STATIC src/bindings.cpp src/VulkanRenderer.cpp) set_target_properties(agave_py2 PROPERTIES OUTPUT_NAME _native ) +if(APPLE) + # Keep bundled dylibs discoverable after the extension is staged into the + # Python package. The build-tree rpaths point at the Vulkan SDK and are + # intentionally removed by CMake during installation. + set_target_properties(agave_py2 PROPERTIES + INSTALL_RPATH "@loader_path" + ) +endif() target_include_directories(agave_py2 PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/.." @@ -42,6 +50,17 @@ else() RUNTIME DESTINATION agave_pyvk COMPONENT agave_pyvk) endif() +if(APPLE) + # Vulkan::Vulkan normally resolves to the SDK's unversioned loader symlink, + # while its Mach-O install name is @rpath/libvulkan.1.dylib. Install the + # complete loader symlink chain so both names and the real file are present. + get_filename_component(_agave_pyvk_vulkan_library_dir "${Vulkan_LIBRARY}" DIRECTORY) + file(GLOB _agave_pyvk_vulkan_loader_files + "${_agave_pyvk_vulkan_library_dir}/libvulkan*.dylib") + install(FILES ${_agave_pyvk_vulkan_loader_files} + DESTINATION agave_pyvk + COMPONENT agave_pyvk) +endif() if(TARGET libCZI) install(TARGETS libCZI LIBRARY DESTINATION agave_pyvk diff --git a/agave_pyvk/CleanStage.cmake b/agave_pyvk/CleanStage.cmake index 9c626a3072..d7826d47d3 100644 --- a/agave_pyvk/CleanStage.cmake +++ b/agave_pyvk/CleanStage.cmake @@ -6,5 +6,8 @@ file(GLOB _agave_pyvk_native_modules "${AGAVE_PYVK_PACKAGE_DIR}/_native*.pyd" "${AGAVE_PYVK_PACKAGE_DIR}/_native*.so" ) -file(REMOVE ${_agave_pyvk_native_modules}) +file(GLOB _agave_pyvk_vulkan_loaders + "${AGAVE_PYVK_PACKAGE_DIR}/libvulkan*.dylib" +) +file(REMOVE ${_agave_pyvk_native_modules} ${_agave_pyvk_vulkan_loaders}) file(REMOVE_RECURSE "${AGAVE_PYVK_PROJECT_DIR}/build") From 3acf30b15c1567fd16f50a1aa43c9df768226a45 Mon Sep 17 00:00:00 2001 From: toloudis Date: Wed, 22 Jul 2026 13:39:22 -0700 Subject: [PATCH 28/35] remove the AGAVE_BUILD_PYVK option --- .github/workflows/build_and_test.yml | 6 ++--- .github/workflows/tagged_master_release.yml | 6 ++--- CMakeLists.txt | 28 ++++++++++----------- README.md | 8 +++--- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 8f5ee6d935..005ca6c97a 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -111,7 +111,7 @@ jobs: CC: gcc-13 CXX: g++-13 run: | - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build --target agave_test --config Release cmake --build build --target install --config Release - uses: actions/upload-artifact@v6 @@ -161,7 +161,7 @@ jobs: env: Qt6_DIR: ${{ runner.workspace }}/Qt/6.9.3/macos run: | - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build --target agave_test --config Release cmake --build build --config Release cpack --config build/CPackConfig.cmake -B build -D CPACK_PACKAGE_FILE_NAME=agave-macos-arm64 @@ -212,7 +212,7 @@ jobs: shell: cmd run: | call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - cmake -S . -B build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake -S . -B build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release if errorlevel 1 exit /b %errorlevel% cmake --build build --parallel --target agave_test --config Release if errorlevel 1 exit /b %errorlevel% diff --git a/.github/workflows/tagged_master_release.yml b/.github/workflows/tagged_master_release.yml index ccb01cd2e7..8a00b414dd 100644 --- a/.github/workflows/tagged_master_release.yml +++ b/.github/workflows/tagged_master_release.yml @@ -41,7 +41,7 @@ jobs: CC: gcc-13 CXX: g++-13 run: | - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build --target agave_test --config Release cmake --build build --target install --config Release - uses: actions/upload-artifact@v6 @@ -94,7 +94,7 @@ jobs: env: Qt6_DIR: ${{ runner.workspace }}/Qt/6.9.3/macos run: | - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build --target agave_test --config Release cmake --build build --config Release cpack --config build/CPackConfig.cmake -B build -D CPACK_PACKAGE_FILE_NAME=agave-macos-arm64 @@ -148,7 +148,7 @@ jobs: shell: cmd run: | call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - cmake -S . -B build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release -DAGAVE_BUILD_PYVK=ON + cmake -S . -B build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE="%VCPKG_INSTALLATION_ROOT%\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows -DCMAKE_BUILD_TYPE=Release if errorlevel 1 exit /b %errorlevel% cmake --build build --parallel --target agave_test --config Release if errorlevel 1 exit /b %errorlevel% diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d31f2b97e..4ea7caaadc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,9 +93,8 @@ find_package(CURL REQUIRED) find_package(OpenGL) -option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) -option(AGAVE_BUILD_PYVK "Build the headless Vulkan Python package" ON) -set(AGAVE_HAS_VULKAN OFF) +option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) +set(AGAVE_HAS_VULKAN OFF) set(AGAVE_VULKAN_RUNTIME_LIBRARY "") if(AGAVE_ENABLE_VULKAN) set(AGAVE_VULKAN_SDK_ROOT "$ENV{VULKAN_SDK}" CACHE PATH "Path to the Vulkan SDK platform directory") @@ -218,20 +217,21 @@ if(AGAVE_ENABLE_VULKAN) endif() endif() message(STATUS "Vulkan backend enabled: SDK ${Vulkan_VERSION}") - elseif(AGAVE_BUILD_PYVK) - message(FATAL_ERROR - "AGAVE_BUILD_PYVK requires Vulkan, glslc, and shaderc_combined. " - "See the Vulkan diagnostics above for the missing component." - ) - else() - message(WARNING "AGAVE_ENABLE_VULKAN is ON, but a required Vulkan component was not found. Vulkan backend disabled.") - endif() + else() + message(FATAL_ERROR + "AGAVE requires Vulkan, glslc, and shaderc_combined. " + "See the Vulkan diagnostics above for the missing component." + ) + endif() unset(_agave_vulkan_glslc_candidates) unset(_agave_vulkan_shaderc_candidates) unset(_agave_vulkan_header_candidates) unset(_agave_vulkan_loader_candidates) unset(_agave_vulkan_shaderc_target_found) -endif() +endif() +if(NOT AGAVE_HAS_VULKAN) + message(FATAL_ERROR "AGAVE_ENABLE_VULKAN must be ON because agave_pyvk is always built") +endif() # normally CMAKE_INSTALL_PREFIX is meant to be outside of the build tree if("${CMAKE_BUILD_TYPE}" STREQUAL "") @@ -249,9 +249,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # ##################### add_subdirectory(renderlib) -if(AGAVE_BUILD_PYVK) - add_subdirectory(agave_pyvk) -endif() +add_subdirectory(agave_pyvk) set(INSTALLDIR "${CMAKE_PROJECT_NAME}-install") diff --git a/README.md b/README.md index b3d7be196c..304a9245ec 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ vcpkg install spdlog zlib libjpeg-turbo liblzma tiff zstd curl --triplet x64-win cmake -S . -B build -G "Ninja Multi-Config" ^ -DCMAKE_TOOLCHAIN_FILE=C:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake ^ -DVCPKG_TARGET_TRIPLET=x64-windows ^ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON cmake --build build --config Release --parallel cmake --install build --config Release ``` @@ -80,7 +80,7 @@ export Qt6_DIR=~/Qt/6.9.3/macos brew install spdlog libtiff nasm curl cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON cmake --build build --parallel cmake --install build ``` @@ -103,7 +103,7 @@ sudo apt install cmake ninja-build libtiff-dev libglm-dev libgl1-mesa-dev \ libzstd-dev zlib1g-dev nasm patch libxcb1-dev cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON -DAGAVE_BUILD_PYVK=ON + -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON cmake --build build --parallel cmake --install build ``` @@ -136,7 +136,7 @@ For a headless-only build tree, use the same platform-specific configure command with these options: ```console --DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF -DAGAVE_BUILD_PYVK=ON +-DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF ``` ## Versioned Releases From d8104e7f2f149882e2f67596517375b4f9bf482e Mon Sep 17 00:00:00 2001 From: toloudis Date: Wed, 22 Jul 2026 09:19:56 -0700 Subject: [PATCH 29/35] try a better way of testing the agave_pyvk importable --- .github/workflows/build_and_test.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 005ca6c97a..801b5cdf66 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -119,10 +119,11 @@ jobs: name: agave-linux path: ./build/install/Release/agave-install - name: Install and test agave_pyvk + working-directory: agave_pyvk run: | - python -m pip install -e "agave_pyvk[test]" + python -m pip install -e ".[test]" python -c "from agave_pyvk import AgaveRenderer; import agave_pyvk._native" - python -m pytest agave_pyvk/tests + python -m pytest tests macos-build: runs-on: macos-15 @@ -170,10 +171,11 @@ jobs: name: agave-macos-arm64 path: ./build/agave-macos-arm64.dmg - name: Install and test agave_pyvk + working-directory: agave_pyvk run: | - python -m pip install -e "agave_pyvk[test]" + python -m pip install -e ".[test]" python -c "from agave_pyvk import AgaveRenderer; import agave_pyvk._native" - python -m pytest agave_pyvk/tests + python -m pytest tests windows-build: runs-on: windows-latest @@ -224,13 +226,14 @@ jobs: name: agave-win path: ./build/agave-win.exe - name: Install and test agave_pyvk + working-directory: agave_pyvk shell: cmd run: | call "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64 - python -m pip install -e "agave_pyvk[test]" + python -m pip install -e ".[test]" if errorlevel 1 exit /b %errorlevel% python -c "from agave_pyvk import AgaveRenderer; import agave_pyvk._native" - python -m pytest agave_pyvk/tests + python -m pytest tests lint-and-test-webclient: runs-on: ubuntu-latest From ec529ba0677c3c18bdc296376bff807512b8d91f Mon Sep 17 00:00:00 2001 From: toloudis Date: Wed, 22 Jul 2026 13:48:58 -0700 Subject: [PATCH 30/35] move the vulkan cmake plumbing into a separate include file --- CMake/ConfigureVulkan.cmake | 137 ++++++++++++++++++++++++++++++++++ CMakeLists.txt | 144 +----------------------------------- 2 files changed, 140 insertions(+), 141 deletions(-) create mode 100644 CMake/ConfigureVulkan.cmake diff --git a/CMake/ConfigureVulkan.cmake b/CMake/ConfigureVulkan.cmake new file mode 100644 index 0000000000..1a8445c30f --- /dev/null +++ b/CMake/ConfigureVulkan.cmake @@ -0,0 +1,137 @@ +# Unconditionally configure the Vulkan SDK used by renderlib and agave_pyvk. +# This file is included at top-level scope so its targets and variables remain +# available to the renderlib and Python subdirectories. + +set(AGAVE_HAS_VULKAN OFF) +set(AGAVE_VULKAN_RUNTIME_LIBRARY "") +set(AGAVE_VULKAN_SDK_ROOT "$ENV{VULKAN_SDK}" CACHE PATH "Path to the Vulkan SDK platform directory") +if(NOT AGAVE_VULKAN_SDK_ROOT) + # No VULKAN_SDK env var and no cached value: look for a locally-installed + # SDK in the platform's default install root and pick the highest version. + if(WIN32) + set(_agave_vulkan_search_glob "C:/VulkanSDK/*") + set(_agave_vulkan_platform_subdir "") + elseif(APPLE) + set(_agave_vulkan_search_glob "$ENV{HOME}/VulkanSDK/*") + set(_agave_vulkan_platform_subdir "macOS") + else() + set(_agave_vulkan_search_glob "$ENV{HOME}/VulkanSDK/*") + set(_agave_vulkan_platform_subdir "x86_64") + endif() + file(GLOB _agave_vulkan_candidates LIST_DIRECTORIES true "${_agave_vulkan_search_glob}") + list(SORT _agave_vulkan_candidates) + list(REVERSE _agave_vulkan_candidates) + foreach(_agave_vulkan_candidate IN LISTS _agave_vulkan_candidates) + if(_agave_vulkan_platform_subdir) + set(_agave_vulkan_candidate "${_agave_vulkan_candidate}/${_agave_vulkan_platform_subdir}") + endif() + if(IS_DIRECTORY "${_agave_vulkan_candidate}" AND EXISTS "${_agave_vulkan_candidate}/include/vulkan/vulkan.h") + set(AGAVE_VULKAN_SDK_ROOT "${_agave_vulkan_candidate}" CACHE PATH "Path to the Vulkan SDK platform directory" FORCE) + break() + endif() + endforeach() + unset(_agave_vulkan_search_glob) + unset(_agave_vulkan_platform_subdir) + unset(_agave_vulkan_candidates) + unset(_agave_vulkan_candidate) +endif() +if(AGAVE_VULKAN_SDK_ROOT) + set(ENV{VULKAN_SDK} "${AGAVE_VULKAN_SDK_ROOT}") + list(INSERT CMAKE_PREFIX_PATH 0 "${AGAVE_VULKAN_SDK_ROOT}") + if(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/include/vulkan/vulkan.h") + set(Vulkan_INCLUDE_DIR "${AGAVE_VULKAN_SDK_ROOT}/include" CACHE PATH "Vulkan SDK include directory" FORCE) + endif() + if(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc") + set(Vulkan_GLSLC_EXECUTABLE "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc" CACHE FILEPATH "Vulkan glslc executable" FORCE) + endif() + if(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/bin/glslangValidator") + set(Vulkan_GLSLANG_VALIDATOR_EXECUTABLE + "${AGAVE_VULKAN_SDK_ROOT}/bin/glslangValidator" + CACHE FILEPATH + "Vulkan glslangValidator executable" + FORCE) + endif() + if(WIN32 AND EXISTS "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan-1.lib") + set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan-1.lib" CACHE FILEPATH "Vulkan loader library" FORCE) + elseif(APPLE AND EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib") + set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib" CACHE FILEPATH "Vulkan loader library" FORCE) + elseif(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so") + set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so" CACHE FILEPATH "Vulkan loader library" FORCE) + endif() +endif() + +file(GLOB _agave_vulkan_glslc_candidates + "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc*" + "${AGAVE_VULKAN_SDK_ROOT}/Bin/glslc*" +) +file(GLOB _agave_vulkan_shaderc_candidates + "${AGAVE_VULKAN_SDK_ROOT}/lib/*shaderc*" + "${AGAVE_VULKAN_SDK_ROOT}/Lib/*shaderc*" +) +file(GLOB _agave_vulkan_header_candidates + "${AGAVE_VULKAN_SDK_ROOT}/include/vulkan/vulkan.h" + "${AGAVE_VULKAN_SDK_ROOT}/Include/vulkan/vulkan.h" +) +file(GLOB _agave_vulkan_loader_candidates + "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan*" + "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan*" +) +message(STATUS "Vulkan diagnostics before find_package:") +message(STATUS " VULKAN_SDK environment: $ENV{VULKAN_SDK}") +message(STATUS " AGAVE_VULKAN_SDK_ROOT: ${AGAVE_VULKAN_SDK_ROOT}") +message(STATUS " Vulkan header files in SDK: ${_agave_vulkan_header_candidates}") +message(STATUS " Vulkan loader files in SDK: ${_agave_vulkan_loader_candidates}") +message(STATUS " glslc files in SDK: ${_agave_vulkan_glslc_candidates}") +message(STATUS " shaderc files in SDK: ${_agave_vulkan_shaderc_candidates}") + +find_package(Vulkan COMPONENTS glslc shaderc_combined) + +if(TARGET Vulkan::shaderc_combined) + set(_agave_vulkan_shaderc_target_found TRUE) +else() + set(_agave_vulkan_shaderc_target_found FALSE) +endif() +message(STATUS "Vulkan diagnostics after find_package:") +message(STATUS " Vulkan_FOUND: ${Vulkan_FOUND}") +message(STATUS " Vulkan_VERSION: ${Vulkan_VERSION}") +message(STATUS " Vulkan_INCLUDE_DIR: ${Vulkan_INCLUDE_DIR}") +message(STATUS " Vulkan_INCLUDE_DIRS: ${Vulkan_INCLUDE_DIRS}") +message(STATUS " Vulkan_LIBRARY: ${Vulkan_LIBRARY}") +message(STATUS " Vulkan_LIBRARIES: ${Vulkan_LIBRARIES}") +message(STATUS " Vulkan_glslc_FOUND: ${Vulkan_glslc_FOUND}") +message(STATUS " Vulkan_GLSLC_EXECUTABLE: ${Vulkan_GLSLC_EXECUTABLE}") +message(STATUS " Vulkan_shaderc_combined_FOUND: ${Vulkan_shaderc_combined_FOUND}") +message(STATUS " Vulkan_shaderc_combined_LIBRARY: ${Vulkan_shaderc_combined_LIBRARY}") +message(STATUS " Vulkan::shaderc_combined target exists: ${_agave_vulkan_shaderc_target_found}") + +if(Vulkan_FOUND AND TARGET Vulkan::shaderc_combined) + set(AGAVE_HAS_VULKAN ON) + set(AGAVE_VULKAN_RUNTIME_LIBRARY "${Vulkan_LIBRARY}") + if(APPLE) + find_library(AGAVE_VULKAN_KOSMICKRISP_LIBRARY + NAMES vulkan_kosmickrisp + PATHS + "${AGAVE_VULKAN_SDK_ROOT}/lib" + "$ENV{VULKAN_SDK}/lib" + "${Vulkan_LIBRARY_DIR}" + NO_DEFAULT_PATH + ) + if(AGAVE_VULKAN_KOSMICKRISP_LIBRARY) + set(AGAVE_VULKAN_RUNTIME_LIBRARY "${AGAVE_VULKAN_KOSMICKRISP_LIBRARY}") + message(STATUS "Using macOS Vulkan runtime: ${AGAVE_VULKAN_RUNTIME_LIBRARY}") + else() + message(WARNING "libvulkan_kosmickrisp.dylib was not found; falling back to ${Vulkan_LIBRARY}") + endif() + endif() + message(STATUS "Vulkan backend enabled: SDK ${Vulkan_VERSION}") +else() + message(FATAL_ERROR + "AGAVE requires Vulkan, glslc, and shaderc_combined. " + "See the Vulkan diagnostics above for the missing component." + ) +endif() +unset(_agave_vulkan_glslc_candidates) +unset(_agave_vulkan_shaderc_candidates) +unset(_agave_vulkan_header_candidates) +unset(_agave_vulkan_loader_candidates) +unset(_agave_vulkan_shaderc_target_found) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ea7caaadc..8afb5cd2fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,147 +93,9 @@ find_package(CURL REQUIRED) find_package(OpenGL) -option(AGAVE_ENABLE_VULKAN "Build the Vulkan graphics backend when the Vulkan SDK is available." ON) -set(AGAVE_HAS_VULKAN OFF) -set(AGAVE_VULKAN_RUNTIME_LIBRARY "") -if(AGAVE_ENABLE_VULKAN) - set(AGAVE_VULKAN_SDK_ROOT "$ENV{VULKAN_SDK}" CACHE PATH "Path to the Vulkan SDK platform directory") - if(NOT AGAVE_VULKAN_SDK_ROOT) - # No VULKAN_SDK env var and no cached value: look for a locally-installed - # SDK in the platform's default install root and pick the highest version. - if(WIN32) - set(_agave_vulkan_search_glob "C:/VulkanSDK/*") - set(_agave_vulkan_platform_subdir "") - elseif(APPLE) - set(_agave_vulkan_search_glob "$ENV{HOME}/VulkanSDK/*") - set(_agave_vulkan_platform_subdir "macOS") - else() - set(_agave_vulkan_search_glob "$ENV{HOME}/VulkanSDK/*") - set(_agave_vulkan_platform_subdir "x86_64") - endif() - file(GLOB _agave_vulkan_candidates LIST_DIRECTORIES true "${_agave_vulkan_search_glob}") - list(SORT _agave_vulkan_candidates) - list(REVERSE _agave_vulkan_candidates) - foreach(_agave_vulkan_candidate IN LISTS _agave_vulkan_candidates) - if(_agave_vulkan_platform_subdir) - set(_agave_vulkan_candidate "${_agave_vulkan_candidate}/${_agave_vulkan_platform_subdir}") - endif() - if(IS_DIRECTORY "${_agave_vulkan_candidate}" AND EXISTS "${_agave_vulkan_candidate}/include/vulkan/vulkan.h") - set(AGAVE_VULKAN_SDK_ROOT "${_agave_vulkan_candidate}" CACHE PATH "Path to the Vulkan SDK platform directory" FORCE) - break() - endif() - endforeach() - unset(_agave_vulkan_search_glob) - unset(_agave_vulkan_platform_subdir) - unset(_agave_vulkan_candidates) - unset(_agave_vulkan_candidate) - endif() - if(AGAVE_VULKAN_SDK_ROOT) - set(ENV{VULKAN_SDK} "${AGAVE_VULKAN_SDK_ROOT}") - list(INSERT CMAKE_PREFIX_PATH 0 "${AGAVE_VULKAN_SDK_ROOT}") - if(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/include/vulkan/vulkan.h") - set(Vulkan_INCLUDE_DIR "${AGAVE_VULKAN_SDK_ROOT}/include" CACHE PATH "Vulkan SDK include directory" FORCE) - endif() - if(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc") - set(Vulkan_GLSLC_EXECUTABLE "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc" CACHE FILEPATH "Vulkan glslc executable" FORCE) - endif() - if(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/bin/glslangValidator") - set(Vulkan_GLSLANG_VALIDATOR_EXECUTABLE - "${AGAVE_VULKAN_SDK_ROOT}/bin/glslangValidator" - CACHE FILEPATH - "Vulkan glslangValidator executable" - FORCE) - endif() - if(WIN32 AND EXISTS "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan-1.lib") - set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan-1.lib" CACHE FILEPATH "Vulkan loader library" FORCE) - elseif(APPLE AND EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib") - set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.dylib" CACHE FILEPATH "Vulkan loader library" FORCE) - elseif(EXISTS "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so") - set(Vulkan_LIBRARY "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan.so" CACHE FILEPATH "Vulkan loader library" FORCE) - endif() - endif() - - file(GLOB _agave_vulkan_glslc_candidates - "${AGAVE_VULKAN_SDK_ROOT}/bin/glslc*" - "${AGAVE_VULKAN_SDK_ROOT}/Bin/glslc*" - ) - file(GLOB _agave_vulkan_shaderc_candidates - "${AGAVE_VULKAN_SDK_ROOT}/lib/*shaderc*" - "${AGAVE_VULKAN_SDK_ROOT}/Lib/*shaderc*" - ) - file(GLOB _agave_vulkan_header_candidates - "${AGAVE_VULKAN_SDK_ROOT}/include/vulkan/vulkan.h" - "${AGAVE_VULKAN_SDK_ROOT}/Include/vulkan/vulkan.h" - ) - file(GLOB _agave_vulkan_loader_candidates - "${AGAVE_VULKAN_SDK_ROOT}/lib/libvulkan*" - "${AGAVE_VULKAN_SDK_ROOT}/Lib/vulkan*" - ) - message(STATUS "Vulkan diagnostics before find_package:") - message(STATUS " VULKAN_SDK environment: $ENV{VULKAN_SDK}") - message(STATUS " AGAVE_VULKAN_SDK_ROOT: ${AGAVE_VULKAN_SDK_ROOT}") - message(STATUS " Vulkan header files in SDK: ${_agave_vulkan_header_candidates}") - message(STATUS " Vulkan loader files in SDK: ${_agave_vulkan_loader_candidates}") - message(STATUS " glslc files in SDK: ${_agave_vulkan_glslc_candidates}") - message(STATUS " shaderc files in SDK: ${_agave_vulkan_shaderc_candidates}") - - find_package(Vulkan COMPONENTS glslc shaderc_combined) - - if(TARGET Vulkan::shaderc_combined) - set(_agave_vulkan_shaderc_target_found TRUE) - else() - set(_agave_vulkan_shaderc_target_found FALSE) - endif() - message(STATUS "Vulkan diagnostics after find_package:") - message(STATUS " Vulkan_FOUND: ${Vulkan_FOUND}") - message(STATUS " Vulkan_VERSION: ${Vulkan_VERSION}") - message(STATUS " Vulkan_INCLUDE_DIR: ${Vulkan_INCLUDE_DIR}") - message(STATUS " Vulkan_INCLUDE_DIRS: ${Vulkan_INCLUDE_DIRS}") - message(STATUS " Vulkan_LIBRARY: ${Vulkan_LIBRARY}") - message(STATUS " Vulkan_LIBRARIES: ${Vulkan_LIBRARIES}") - message(STATUS " Vulkan_glslc_FOUND: ${Vulkan_glslc_FOUND}") - message(STATUS " Vulkan_GLSLC_EXECUTABLE: ${Vulkan_GLSLC_EXECUTABLE}") - message(STATUS " Vulkan_shaderc_combined_FOUND: ${Vulkan_shaderc_combined_FOUND}") - message(STATUS " Vulkan_shaderc_combined_LIBRARY: ${Vulkan_shaderc_combined_LIBRARY}") - message(STATUS " Vulkan::shaderc_combined target exists: ${_agave_vulkan_shaderc_target_found}") - - if(Vulkan_FOUND AND TARGET Vulkan::shaderc_combined) - set(AGAVE_HAS_VULKAN ON) - set(AGAVE_VULKAN_RUNTIME_LIBRARY "${Vulkan_LIBRARY}") - if(APPLE) - find_library(AGAVE_VULKAN_KOSMICKRISP_LIBRARY - NAMES vulkan_kosmickrisp - PATHS - "${AGAVE_VULKAN_SDK_ROOT}/lib" - "$ENV{VULKAN_SDK}/lib" - "${Vulkan_LIBRARY_DIR}" - NO_DEFAULT_PATH - ) - if(AGAVE_VULKAN_KOSMICKRISP_LIBRARY) - set(AGAVE_VULKAN_RUNTIME_LIBRARY "${AGAVE_VULKAN_KOSMICKRISP_LIBRARY}") - message(STATUS "Using macOS Vulkan runtime: ${AGAVE_VULKAN_RUNTIME_LIBRARY}") - else() - message(WARNING "libvulkan_kosmickrisp.dylib was not found; falling back to ${Vulkan_LIBRARY}") - endif() - endif() - message(STATUS "Vulkan backend enabled: SDK ${Vulkan_VERSION}") - else() - message(FATAL_ERROR - "AGAVE requires Vulkan, glslc, and shaderc_combined. " - "See the Vulkan diagnostics above for the missing component." - ) - endif() - unset(_agave_vulkan_glslc_candidates) - unset(_agave_vulkan_shaderc_candidates) - unset(_agave_vulkan_header_candidates) - unset(_agave_vulkan_loader_candidates) - unset(_agave_vulkan_shaderc_target_found) -endif() -if(NOT AGAVE_HAS_VULKAN) - message(FATAL_ERROR "AGAVE_ENABLE_VULKAN must be ON because agave_pyvk is always built") -endif() - -# normally CMAKE_INSTALL_PREFIX is meant to be outside of the build tree +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ConfigureVulkan.cmake") + +# normally CMAKE_INSTALL_PREFIX is meant to be outside of the build tree if("${CMAKE_BUILD_TYPE}" STREQUAL "") set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install) else() From 52a46384cd7340a2434e8d4ea0751ac9425d8aca Mon Sep 17 00:00:00 2001 From: toloudis Date: Wed, 22 Jul 2026 16:03:09 -0700 Subject: [PATCH 31/35] fix dir name --- CMakeLists.txt | 25 +++++++++++++------------ agave_app/CMakeLists.txt | 4 ---- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8afb5cd2fe..8f404fdeeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,10 +40,10 @@ project( LANGUAGES C CXX ) -option(AGAVE_BUILD_APP "Build the Qt desktop application and installers" ON) -option(AGAVE_BUILD_TESTS "Build the C++ test suite" ON) - -set(CMAKE_CXX_STANDARD 17) +option(AGAVE_BUILD_APP "Build the Qt desktop application and installers" ON) +option(AGAVE_BUILD_TESTS "Build the C++ test suite" ON) + +set(CMAKE_CXX_STANDARD 17) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) IF(WIN32) @@ -93,9 +93,10 @@ find_package(CURL REQUIRED) find_package(OpenGL) -include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ConfigureVulkan.cmake") - -# normally CMAKE_INSTALL_PREFIX is meant to be outside of the build tree +# this is my find_package(Vulkan) with some extra checks +include("${CMAKE_CURRENT_SOURCE_DIR}/CMake/ConfigureVulkan.cmake") + +# normally CMAKE_INSTALL_PREFIX is meant to be outside of the build tree if("${CMAKE_BUILD_TYPE}" STREQUAL "") set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install) else() @@ -103,23 +104,23 @@ else() endif() message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # ##################### # THE COMMON CORE LIBRARIES # ##################### add_subdirectory(renderlib) -add_subdirectory(agave_pyvk) +add_subdirectory(agave_pyvk) set(INSTALLDIR "${CMAKE_PROJECT_NAME}-install") message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") message(STATUS "INSTALLDIR: ${INSTALLDIR}") message(STATUS "CMAKE_BINARY_DIR: ${CMAKE_BINARY_DIR}") -message(STATUS "CMAKE_LIBRARY_OUTPUT_DIRECTORY: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -message(STATUS "CMAKE_RUNTIME_OUTPUT_DIRECTORY: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") +message(STATUS "CMAKE_LIBRARY_OUTPUT_DIRECTORY: ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +message(STATUS "CMAKE_RUNTIME_OUTPUT_DIRECTORY: ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}") # ##################### # UNIT TESTING diff --git a/agave_app/CMakeLists.txt b/agave_app/CMakeLists.txt index 080a21989e..838b6eaad5 100644 --- a/agave_app/CMakeLists.txt +++ b/agave_app/CMakeLists.txt @@ -1,8 +1,4 @@ # Find the Qt libraries -# In order for find_package to be successful, Qt 5 must be found below the CMAKE_PREFIX_PATH, -# or the Qt6_DIR must be set in the CMake cache to the location of the Qt6WidgetsConfig.cmake -# file. The easiest way to use CMake is to set the CMAKE_PREFIX_PATH environment variable to -# the install prefix of Qt 6. if(DEFINED ENV{Qt6_DIR}) list(INSERT CMAKE_PREFIX_PATH 0 $ENV{Qt6_DIR}) endif() From 46d4f63321ac4b025be229f69167852a39acf214 Mon Sep 17 00:00:00 2001 From: toloudis Date: Thu, 23 Jul 2026 08:08:54 -0700 Subject: [PATCH 32/35] simplify /clarify readmes --- README.md | 40 +++++++++++++++++++++++++--------------- agave_pyvk/README.md | 5 +++-- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 304a9245ec..6ca3923346 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,14 @@ After cloning this repo, initialize the submodules, which contain a couple of de git submodule update --init ``` -The commands below build the desktop application, C++ tests, renderlib, and the -standalone `agave_pyvk` native module. Building `agave_pyvk` requires a Vulkan -1.3-capable driver and a Vulkan SDK. CMake uses the SDK named by `VULKAN_SDK`, -or searches the platform's usual Vulkan SDK installation directory. Use Python -3.10 or later when building the standalone package. +The commands below build the desktop application, unit tests, and the +Python native module (use Python 3.10 or later). +CMake options are provided for AGAVE_BUILD_APP and AGAVE_BUILD_TESTS, and both default to "ON". + +Building requires the Vulkan SDK. CMake uses the SDK named by `VULKAN_SDK`, +or searches the platform's usual Vulkan SDK installation directory. + +You may also refer to the github actions workflows for reference building AGAVE. ### Windows @@ -59,15 +62,19 @@ vcpkg install spdlog zlib libjpeg-turbo liblzma tiff zstd curl --triplet x64-win ```console cmake -S . -B build -G "Ninja Multi-Config" ^ -DCMAKE_TOOLCHAIN_FILE=C:\path\to\vcpkg\scripts\buildsystems\vcpkg.cmake ^ - -DVCPKG_TARGET_TRIPLET=x64-windows ^ - -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON + -DVCPKG_TARGET_TRIPLET=x64-windows cmake --build build --config Release --parallel cmake --install build --config Release + +# you may also choose --config RelWithDebInfo or Debug +# for day to day Windows dev / debugging of the app executable +# Alternative build command: +cmake --build build --target install ``` -You may need to adjust the vcpkg path depending on your configuration. +Adjust the vcpkg path depending on your configuration. -If you encounter issues during your build, check that all of your dependencies are installed and try again. You can also build to the INSTALL target with Visual Studio by opening the project solution (`agave.sln`). +If you encounter issues during your build, check that all of your dependencies are installed and try again. It can be helpful to delete the \_deps subdirectory in your build path. ### macOS Apple Silicon (Homebrew) @@ -82,6 +89,7 @@ brew install spdlog libtiff nasm curl cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON cmake --build build --parallel +# optional cmake --install build ``` @@ -105,16 +113,18 @@ sudo apt install cmake ninja-build libtiff-dev libglm-dev libgl1-mesa-dev \ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DAGAVE_BUILD_APP=ON -DAGAVE_BUILD_TESTS=ON cmake --build build --parallel +# optional cmake --install build ``` -If cmake fails please refer to the Dockerfile for a more complete list of Linux dependencies. +If cmake fails please refer to the Dockerfile or the github actions workflows for a more complete list of Linux dependencies. ### Iterative standalone Python development The platform commands above create a persistent `build` directory. CMake uses -the active Python interpreter and its default build compiles renderlib, the -desktop application, and the ABI-specific `agave_py2` native module: +the active Python interpreter and C++ compiler environment, and its default +build compiles the shared static renderlib, the desktop application, +and the ABI-specific `agave_py2` native module: ```console cmake --build . @@ -132,11 +142,11 @@ in the Python package. The editable install performs only normal Python packaging; it does not invoke CMake or rebuild renderlib. Python source changes are visible immediately. -For a headless-only build tree, use the same platform-specific configure command -with these options: +Optional: To save a little time in building the Python module only, use the same +platform-specific CMake configure command with these options: ```console --DAGAVE_BUILD_APP=OFF -DAGAVE_BUILD_TESTS=OFF +-DAGAVE_BUILD_APP=OFF ``` ## Versioned Releases diff --git a/agave_pyvk/README.md b/agave_pyvk/README.md index c620f91c2e..9759ff930a 100644 --- a/agave_pyvk/README.md +++ b/agave_pyvk/README.md @@ -1,8 +1,7 @@ # agave_pyvk `agave_pyvk` is the standalone, headless, in-process Vulkan Python package for -AGAVE. Its `AgaveRenderer` API mirrors `agave_pyclient`, but commands cross a -nanobind boundary and execute synchronously in renderlib. It has no Qt or +AGAVE. It uses nanobind to execute Python commands directly in renderlib. It has no Qt or WebSocket dependency. ```python @@ -39,6 +38,8 @@ The CMake build stages `_native` and its runtime dependencies into the package directory. Pip performs only normal Python packaging and does not invoke CMake. Python source edits are visible immediately. +From this point you can import agave_pyvk and its AgaveRenderer class. + To produce a wheel file from the same staged native build, install the `build` frontend and run: From a86649891da62909a250f0c524f46fe263dc0af4 Mon Sep 17 00:00:00 2001 From: toloudis Date: Thu, 23 Jul 2026 08:32:40 -0700 Subject: [PATCH 33/35] cleanup --- agave_pyvk/src/VulkanRenderer.cpp | 291 +++++++++++++++--------------- agave_pyvk/src/VulkanRenderer.h | 1 + 2 files changed, 147 insertions(+), 145 deletions(-) diff --git a/agave_pyvk/src/VulkanRenderer.cpp b/agave_pyvk/src/VulkanRenderer.cpp index 3d0ae914be..d3d0b7ca8d 100644 --- a/agave_pyvk/src/VulkanRenderer.cpp +++ b/agave_pyvk/src/VulkanRenderer.cpp @@ -47,6 +47,34 @@ requireArgs(int commandId, const nb::args& args, size_t expected) } } +LoadDataCommandD +makeLoadDataCommandData(const nb::args& args) +{ + const auto region = arg>(args, 5); + LoadDataCommandD data{ arg(args, 0), + arg(args, 1), + arg(args, 2), + arg(args, 3), + arg>(args, 4), + 0, + 0, + 0, + 0, + 0, + 0 }; + if (region.size() == 6) { + data.m_xmin = region[0]; + data.m_xmax = region[1]; + data.m_ymin = region[2]; + data.m_ymax = region[3]; + data.m_zmin = region[4]; + data.m_zmax = region[5]; + } else if (!region.empty()) { + throw nb::value_error("LOAD_DATA region must be empty or contain six integers"); + } + return data; +} + } // namespace VulkanRenderer::VulkanRenderer(const std::string& mode, const std::string& assetPath, int selectedGpu) @@ -173,6 +201,13 @@ VulkanRenderer::run(Command& command) return nb::none(); } +nb::object +VulkanRenderer::run(RequestRedrawCommand& command) +{ + run(static_cast(command)); + return redraw(); +} + nb::object VulkanRenderer::redraw() { @@ -208,7 +243,7 @@ VulkanRenderer::execute(int commandId, const nb::args& args) { #define EXECUTE_COMMAND(ID, COUNT, TYPE, ...) \ case ID: { \ - requireArgs(ID, args, COUNT); \ + requireArgs(ID, args, TYPE::ArgTypes().size()); \ TYPE command(__VA_ARGS__); \ return run(command); \ } @@ -250,150 +285,116 @@ VulkanRenderer::execute(int commandId, const nb::args& args) arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3), arg(args, 4) }); EXECUTE_COMMAND(13, 1, SetRenderIterationsCommand, SetRenderIterationsCommandD{ arg(args, 0) }); EXECUTE_COMMAND(14, 1, SetStreamModeCommand, SetStreamModeCommandD{ arg(args, 0) }); - case 15: - requireArgs(15, args, 0); - return redraw(); - EXECUTE_COMMAND( - 16, 2, SetResolutionCommand, SetResolutionCommandD{ arg(args, 0), arg(args, 1) }); - EXECUTE_COMMAND(17, 1, SetDensityCommand, SetDensityCommandD{ arg(args, 0) }); - EXECUTE_COMMAND(18, 0, FrameSceneCommand, FrameSceneCommandD{}); - EXECUTE_COMMAND(19, 2, SetGlossinessCommand, SetGlossinessCommandD{ arg(args, 0), arg(args, 1) }); - EXECUTE_COMMAND( - 20, 2, EnableChannelCommand, EnableChannelCommandD{ arg(args, 0), arg(args, 1) }); - EXECUTE_COMMAND(21, - 3, - SetWindowLevelCommand, - SetWindowLevelCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(22, 2, OrbitCameraCommand, OrbitCameraCommandD{ arg(args, 0), arg(args, 1) }); - EXECUTE_COMMAND(23, - 3, - SetSkylightTopColorCommand, - SetSkylightTopColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(24, - 3, - SetSkylightMiddleColorCommand, - SetSkylightMiddleColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(25, - 3, - SetSkylightBottomColorCommand, - SetSkylightBottomColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND( - 26, - 4, - SetLightPosCommand, - SetLightPosCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); - EXECUTE_COMMAND( - 27, - 4, - SetLightColorCommand, - SetLightColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); - EXECUTE_COMMAND(28, - 3, - SetLightSizeCommand, - SetLightSizeCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(29, - 6, - SetClipRegionCommand, - SetClipRegionCommandD{ arg(args, 0), - arg(args, 1), - arg(args, 2), - arg(args, 3), - arg(args, 4), - arg(args, 5) }); - EXECUTE_COMMAND(30, - 3, - SetVoxelScaleCommand, - SetVoxelScaleCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND( - 31, 2, AutoThresholdCommand, AutoThresholdCommandD{ arg(args, 0), arg(args, 1) }); - EXECUTE_COMMAND( - 32, - 3, - SetPercentileThresholdCommand, - SetPercentileThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(33, 2, SetOpacityCommand, SetOpacityCommandD{ arg(args, 0), arg(args, 1) }); - EXECUTE_COMMAND(34, 1, SetPrimaryRayStepSizeCommand, SetPrimaryRayStepSizeCommandD{ arg(args, 0) }); - EXECUTE_COMMAND(35, 1, SetSecondaryRayStepSizeCommand, SetSecondaryRayStepSizeCommandD{ arg(args, 0) }); - EXECUTE_COMMAND(36, - 3, - SetBackgroundColorCommand, - SetBackgroundColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(37, - 3, - SetIsovalueThresholdCommand, - SetIsovalueThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(38, - 2, - SetControlPointsCommand, - SetControlPointsCommandD{ arg(args, 0), arg>(args, 1) }); - EXECUTE_COMMAND( - 39, - 3, - LoadVolumeFromFileCommand, - LoadVolumeFromFileCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(40, 1, SetTimeCommand, SetTimeCommandD{ arg(args, 0) }); - EXECUTE_COMMAND(41, - 3, - SetBoundingBoxColorCommand, - SetBoundingBoxColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(42, 1, ShowBoundingBoxCommand, ShowBoundingBoxCommandD{ arg(args, 0) }); - EXECUTE_COMMAND( - 43, 2, TrackballCameraCommand, TrackballCameraCommandD{ arg(args, 0), arg(args, 1) }); - case 44: { - requireArgs(44, args, 6); - const auto region = arg>(args, 5); - LoadDataCommandD data{ arg(args, 0), - arg(args, 1), - arg(args, 2), - arg(args, 3), - arg>(args, 4), - 0, - 0, - 0, - 0, - 0, - 0 }; - if (region.size() == 6) { - data.m_xmin = region[0]; - data.m_xmax = region[1]; - data.m_ymin = region[2]; - data.m_ymax = region[3]; - data.m_zmin = region[4]; - data.m_zmax = region[5]; - } else if (!region.empty()) { - throw nb::value_error("LOAD_DATA region must be empty or contain six integers"); - } - LoadDataCommand command(std::move(data)); - return run(command); - } - EXECUTE_COMMAND(45, 1, ShowScaleBarCommand, ShowScaleBarCommandD{ arg(args, 0) }); - EXECUTE_COMMAND(46, - 3, - SetFlipAxisCommand, - SetFlipAxisCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(47, 1, SetInterpolationCommand, SetInterpolationCommandD{ arg(args, 0) }); - EXECUTE_COMMAND( - 48, - 4, - SetClipPlaneCommand, - SetClipPlaneCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); - EXECUTE_COMMAND( - 49, - 3, - SetColorRampCommand, - SetColorRampCommandD{ arg(args, 0), arg(args, 1), arg>(args, 2) }); - EXECUTE_COMMAND( - 50, - 3, - SetMinMaxThresholdCommand, - SetMinMaxThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); - EXECUTE_COMMAND(51, - 4, - SetSkylightRotationCommand, - SetSkylightRotationCommandD{ - arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); - EXECUTE_COMMAND(52, 1, ShowTimeStampCommand, ShowTimeStampCommandD{ arg(args, 0) }); - EXECUTE_COMMAND(53, 1, SetTimeStampFormatCommand, SetTimeStampFormatCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(15, 0, RequestRedrawCommand, RequestRedrawCommandD{}); + EXECUTE_COMMAND(16, 2, SetResolutionCommand, SetResolutionCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(17, 1, SetDensityCommand, SetDensityCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(18, 0, FrameSceneCommand, FrameSceneCommandD{}); + EXECUTE_COMMAND(19, 2, SetGlossinessCommand, SetGlossinessCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(20, 2, EnableChannelCommand, EnableChannelCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(21, + 3, + SetWindowLevelCommand, + SetWindowLevelCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(22, 2, OrbitCameraCommand, OrbitCameraCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(23, + 3, + SetSkylightTopColorCommand, + SetSkylightTopColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(24, + 3, + SetSkylightMiddleColorCommand, + SetSkylightMiddleColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(25, + 3, + SetSkylightBottomColorCommand, + SetSkylightBottomColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND( + 26, + 4, + SetLightPosCommand, + SetLightPosCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); + EXECUTE_COMMAND( + 27, + 4, + SetLightColorCommand, + SetLightColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); + EXECUTE_COMMAND(28, + 3, + SetLightSizeCommand, + SetLightSizeCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(29, + 6, + SetClipRegionCommand, + SetClipRegionCommandD{ arg(args, 0), + arg(args, 1), + arg(args, 2), + arg(args, 3), + arg(args, 4), + arg(args, 5) }); + EXECUTE_COMMAND(30, + 3, + SetVoxelScaleCommand, + SetVoxelScaleCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(31, 2, AutoThresholdCommand, AutoThresholdCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(32, + 3, + SetPercentileThresholdCommand, + SetPercentileThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(33, 2, SetOpacityCommand, SetOpacityCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(34, 1, SetPrimaryRayStepSizeCommand, SetPrimaryRayStepSizeCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(35, 1, SetSecondaryRayStepSizeCommand, SetSecondaryRayStepSizeCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(36, + 3, + SetBackgroundColorCommand, + SetBackgroundColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(37, + 3, + SetIsovalueThresholdCommand, + SetIsovalueThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(38, + 2, + SetControlPointsCommand, + SetControlPointsCommandD{ arg(args, 0), arg>(args, 1) }); + EXECUTE_COMMAND( + 39, + 3, + LoadVolumeFromFileCommand, + LoadVolumeFromFileCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(40, 1, SetTimeCommand, SetTimeCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(41, + 3, + SetBoundingBoxColorCommand, + SetBoundingBoxColorCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(42, 1, ShowBoundingBoxCommand, ShowBoundingBoxCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(43, 2, TrackballCameraCommand, TrackballCameraCommandD{ arg(args, 0), arg(args, 1) }); + EXECUTE_COMMAND(44, 6, LoadDataCommand, makeLoadDataCommandData(args)); + EXECUTE_COMMAND(45, 1, ShowScaleBarCommand, ShowScaleBarCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(46, + 3, + SetFlipAxisCommand, + SetFlipAxisCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(47, 1, SetInterpolationCommand, SetInterpolationCommandD{ arg(args, 0) }); + EXECUTE_COMMAND( + 48, + 4, + SetClipPlaneCommand, + SetClipPlaneCommandD{ arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); + EXECUTE_COMMAND( + 49, + 3, + SetColorRampCommand, + SetColorRampCommandD{ arg(args, 0), arg(args, 1), arg>(args, 2) }); + EXECUTE_COMMAND(50, + 3, + SetMinMaxThresholdCommand, + SetMinMaxThresholdCommandD{ arg(args, 0), arg(args, 1), arg(args, 2) }); + EXECUTE_COMMAND(51, + 4, + SetSkylightRotationCommand, + SetSkylightRotationCommandD{ + arg(args, 0), arg(args, 1), arg(args, 2), arg(args, 3) }); + EXECUTE_COMMAND(52, 1, ShowTimeStampCommand, ShowTimeStampCommandD{ arg(args, 0) }); + EXECUTE_COMMAND(53, 1, SetTimeStampFormatCommand, SetTimeStampFormatCommandD{ arg(args, 0) }); default: throw nb::value_error(("Unknown AGAVE command id " + std::to_string(commandId)).c_str()); } diff --git a/agave_pyvk/src/VulkanRenderer.h b/agave_pyvk/src/VulkanRenderer.h index 1fd6bb7429..50941fd869 100644 --- a/agave_pyvk/src/VulkanRenderer.h +++ b/agave_pyvk/src/VulkanRenderer.h @@ -35,6 +35,7 @@ class VulkanRenderer final : public RendererCommandInterface private: nanobind::object run(Command& command); + nanobind::object run(RequestRedrawCommand& command); nanobind::object redraw(); void initialize(const std::string& mode, const std::string& assetPath, int selectedGpu); From e0d31d5e5ff2f7506e87b2d4be4f951d7ac21dd8 Mon Sep 17 00:00:00 2001 From: toloudis Date: Thu, 23 Jul 2026 09:00:09 -0700 Subject: [PATCH 34/35] refactor to push more implementation back into renderlib --- agave_pyvk/CMakeLists.txt | 2 +- agave_pyvk/agave_pyvk/agave.py | 2 +- agave_pyvk/agave_pyvk/example1.py | 49 +++++ agave_pyvk/agave_pyvk/example2.py | 122 +++++++++++++ agave_pyvk/agave_pyvk/example3.py | 109 ++++++++++++ agave_pyvk/src/VulkanRenderer.h | 54 ------ agave_pyvk/src/bindings.cpp | 78 +++++++- agave_pyvk/tests/test_api.py | 4 +- renderlib/CMakeLists.txt | 8 +- .../PythonRenderer.cpp | 167 +++++++++++++----- renderlib/PythonRenderer.h | 81 +++++++++ 11 files changed, 562 insertions(+), 114 deletions(-) create mode 100644 agave_pyvk/agave_pyvk/example1.py create mode 100644 agave_pyvk/agave_pyvk/example2.py create mode 100644 agave_pyvk/agave_pyvk/example3.py delete mode 100644 agave_pyvk/src/VulkanRenderer.h rename agave_pyvk/src/VulkanRenderer.cpp => renderlib/PythonRenderer.cpp (71%) create mode 100644 renderlib/PythonRenderer.h diff --git a/agave_pyvk/CMakeLists.txt b/agave_pyvk/CMakeLists.txt index a04892f85a..89eff805b4 100644 --- a/agave_pyvk/CMakeLists.txt +++ b/agave_pyvk/CMakeLists.txt @@ -15,7 +15,7 @@ if(NOT AGAVE_HAS_VULKAN) message(FATAL_ERROR "agave_pyvk requires an AGAVE renderlib target with Vulkan enabled") endif() -nanobind_add_module(agave_py2 NB_STATIC src/bindings.cpp src/VulkanRenderer.cpp) +nanobind_add_module(agave_py2 NB_STATIC src/bindings.cpp) set_target_properties(agave_py2 PROPERTIES OUTPUT_NAME _native ) diff --git a/agave_pyvk/agave_pyvk/agave.py b/agave_pyvk/agave_pyvk/agave.py index 38c8ffff52..1ab189d988 100644 --- a/agave_pyvk/agave_pyvk/agave.py +++ b/agave_pyvk/agave_pyvk/agave.py @@ -89,7 +89,7 @@ def __init__( if mode not in ("pathtrace", "raymarch"): mode = "pathtrace" asset_path = Path(_native.__file__).resolve().parent / "assets" - self._renderer = _native.VulkanRenderer(mode, str(asset_path), 0) + self._renderer = _native.PythonRenderer(mode, str(asset_path), 0) self.session_name = "" def __enter__(self): diff --git a/agave_pyvk/agave_pyvk/example1.py b/agave_pyvk/agave_pyvk/example1.py new file mode 100644 index 0000000000..b77e92d6ff --- /dev/null +++ b/agave_pyvk/agave_pyvk/example1.py @@ -0,0 +1,49 @@ +from agave_pyvk import AgaveRenderer + +# imgplot = plt.imshow(numpy.zeros((1024, 768))) +if __name__ == "__main__": + filename = ( + "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/variance/1.zarr" + ) + # AGAVE is already running in server mode. Connect to it. + # Note that it could be remote. + r = AgaveRenderer(url="ws://localhost:1235/", mode="raymarch") + r.load_data(filename, 0, 0, 0) + r.set_resolution(512, 512) + r.set_voxel_scale(1.0, -1.0, 2.9) + r.render_iterations(128) + r.set_clip_region(0, 1, 0, 1, 0, 1) + r.camera_projection(1, 55) + r.frame_scene() + r.exposure(0.8714) + r.density(100) + r.aperture(0) + r.focaldist(0.75) + r.enable_channel(0, 0) + r.mat_diffuse(0, 1, 0, 1, 1.0) + r.mat_specular(0, 0, 0, 0, 0.0) + r.mat_emissive(0, 0, 0, 0, 0.0) + r.mat_glossiness(0, 0) + r.set_window_level(0, 1, 0.758) + r.enable_channel(1, 1) + r.mat_diffuse(1, 1, 1, 1, 1.0) + r.mat_specular(1, 0, 0, 0, 0.0) + r.mat_emissive(1, 0, 0, 0, 0.0) + r.mat_glossiness(1, 0) + r.set_window_level(1, 1, 0.811) + r.enable_channel(2, 1) + r.mat_diffuse(2, 0, 1, 1, 1.0) + r.mat_specular(2, 0, 0, 0, 0.0) + r.mat_emissive(2, 0, 0, 0, 0.0) + r.mat_glossiness(2, 0) + r.set_window_level(2, 0.9922, 0.7704) + r.skylight_top_color(0.5, 0.5, 0.5) + r.skylight_middle_color(0.5, 0.5, 0.5) + r.skylight_bottom_color(0.5, 0.5, 0.5) + r.light_pos(0, 10.1663, 1.1607, 0.5324) + r.light_color(0, 122.926, 122.926, 125.999) + r.light_size(0, 1, 1) + r.session("out.png") + r.redraw() + # r.batch_render_rocker(number_of_frames=40, angle=30, output_name="rocker") + # r.batch_render_turntable(number_of_frames=90, output_name="turntable") diff --git a/agave_pyvk/agave_pyvk/example2.py b/agave_pyvk/agave_pyvk/example2.py new file mode 100644 index 0000000000..fdd1b5a8f5 --- /dev/null +++ b/agave_pyvk/agave_pyvk/example2.py @@ -0,0 +1,122 @@ +from agave_pyvk import AgaveRenderer + + +def main(): + # r = AgaveRenderer(url="ws://localhost:1235/", mode="pathtrace") + r = AgaveRenderer(agave_path="D:\\agave_build\\install\\agave-install\\agave.exe") + with r: + r.load_data( + "https://animatedcell-test-data.s3.us-west-2.amazonaws.com/variance/1.zarr", + 0, + 2, + 0, + [0, 1, 2, 3, 4, 5, 6, 7, 8], + [0, 231, 0, 156, 0, 65], + ) + r.set_resolution(1448, 996) + r.background_color(0, 0, 0) + r.show_bounding_box(1) + r.show_scale_bar(0) + r.bounding_box_color(1, 1, 1) + r.render_iterations(32) + r.set_primary_ray_step_size(4) + r.set_secondary_ray_step_size(4) + r.set_interpolation(1) + r.set_voxel_scale(0.433333, 0.433333, 0.29) + r.set_flip_axis(1, 1, 1) + r.set_clip_region(0, 1, 0, 1, 0, 1) + r.set_clip_plane(0, 0, 0, 0) + r.eye(0.5, 0.337662, 1.26718) + r.target(0.5, 0.337662, 0.0941558) + r.up(0, 1, 0) + r.camera_projection(1, 0.5) + r.exposure(0.75) + r.density(100) + r.aperture(0) + r.focaldist(0.75) + r.enable_channel(0, 1) + r.mat_diffuse(0, 1, 0, 1, 1) + r.mat_specular(0, 0, 0, 0, 0) + r.mat_emissive(0, 0, 0, 0, 0) + r.mat_glossiness(0, 1) + r.mat_opacity(0, 1) + r.set_window_level(0, 0.25, 0.1807) + r.set_color_ramp(0, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.enable_channel(1, 1) + r.mat_diffuse(1, 1, 1, 1, 1) + r.mat_specular(1, 0, 0, 0, 0) + r.mat_emissive(1, 0, 0, 0, 0) + r.mat_glossiness(1, 1) + r.mat_opacity(1, 1) + r.set_window_level(1, 0.25, 0.2289) + r.set_color_ramp(1, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.enable_channel(2, 1) + r.mat_diffuse(2, 0, 1, 1, 1) + r.mat_specular(2, 0, 0, 0, 0) + r.mat_emissive(2, 0, 0, 0, 0) + r.mat_glossiness(2, 1) + r.mat_opacity(2, 1) + r.set_window_level(2, 0.25, 0.2289) + r.set_color_ramp(2, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.enable_channel(3, 0) + r.mat_diffuse(3, 0.500008, 0, 0, 1) + r.mat_specular(3, 0, 0, 0, 0) + r.mat_emissive(3, 0, 0, 0, 0) + r.mat_glossiness(3, 1) + r.mat_opacity(3, 1) + r.set_percentile_threshold(3, 0.5, 0.98) + r.set_color_ramp(3, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.enable_channel(4, 0) + r.mat_diffuse(4, 0, 0.145892, 0.500008, 1) + r.mat_specular(4, 0, 0, 0, 0) + r.mat_emissive(4, 0, 0, 0, 0) + r.mat_glossiness(4, 1) + r.mat_opacity(4, 1) + r.set_percentile_threshold(4, 0.5, 0.98) + r.set_color_ramp(4, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.enable_channel(5, 0) + r.mat_diffuse(5, 0.291798, 0.500008, 0, 1) + r.mat_specular(5, 0, 0, 0, 0) + r.mat_emissive(5, 0, 0, 0, 0) + r.mat_glossiness(5, 1) + r.mat_opacity(5, 1) + r.set_percentile_threshold(5, 0.5, 0.98) + r.set_color_ramp(5, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.enable_channel(6, 0) + r.mat_diffuse(6, 0.500008, 0, 0.43769, 1) + r.mat_specular(6, 0, 0, 0, 0) + r.mat_emissive(6, 0, 0, 0, 0) + r.mat_glossiness(6, 1) + r.mat_opacity(6, 1) + r.set_percentile_threshold(6, 0.5, 0.98) + r.set_color_ramp(6, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.enable_channel(7, 0) + r.mat_diffuse(7, 0, 0.500008, 0.416403, 1) + r.mat_specular(7, 0, 0, 0, 0) + r.mat_emissive(7, 0, 0, 0, 0) + r.mat_glossiness(7, 1) + r.mat_opacity(7, 1) + r.set_percentile_threshold(7, 0.5, 0.98) + r.set_color_ramp(7, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.enable_channel(8, 0) + r.mat_diffuse(8, 0.500008, 0.270512, 0, 1) + r.mat_specular(8, 0, 0, 0, 0) + r.mat_emissive(8, 0, 0, 0, 0) + r.mat_glossiness(8, 1) + r.mat_opacity(8, 1) + r.set_percentile_threshold(8, 0.5, 0.98) + r.set_color_ramp(8, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + r.skylight_top_color(1, 1, 1) + r.skylight_middle_color(1, 1, 1) + r.skylight_bottom_color(1, 1, 1) + r.light_pos(0, 1.5, 5.54884, 0.952752) + r.light_color(0, 10, 10, 10) + r.light_size(0, 0.15, 0.15) + for i in range(1, 65): + r.set_clip_region(0, 1, 0, 1, 0, i / 65) + r.session(f"1_{i}.zarr.png") + r.redraw() + + +if __name__ == "__main__": + main() diff --git a/agave_pyvk/agave_pyvk/example3.py b/agave_pyvk/agave_pyvk/example3.py new file mode 100644 index 0000000000..f27cf90228 --- /dev/null +++ b/agave_pyvk/agave_pyvk/example3.py @@ -0,0 +1,109 @@ +# pip install agave_pyclient +# agave --server & +# python myscript.py + +import agave_pyvk as agave + +r = agave.AgaveRenderer(mode="pathtrace") +r.load_data("https://animatedcell-test-data.s3.us-west-2.amazonaws.com/variance/1.zarr", 0, 2, 0, [0, 1, 2, 3, 4, 5, 6, 7, 8], [0, 231, 0, 156, 0, 65]) +r.set_resolution(1448, 996) +r.background_color(0, 0, 0) +r.show_bounding_box(1) +r.show_scale_bar(0) +r.bounding_box_color(1, 1, 1) +r.render_iterations(32) +r.set_primary_ray_step_size(4) +r.set_secondary_ray_step_size(4) +r.set_interpolation(1) +r.set_voxel_scale(0.433333, 0.433333, 0.29) +r.set_flip_axis(1, 1, 1) +r.set_clip_region(0, 1, 0, 1, 0, 1) +r.set_clip_plane(0, 0, 0, 0) +r.eye(0.5, 0.337662, 1.26718) +r.target(0.5, 0.337662, 0.0941558) +r.up(0, 1, 0) +r.camera_projection(1, 0.5) +r.exposure(0.75) +r.density(100) +r.aperture(0) +r.focaldist(0.75) +r.enable_channel(0, 1) +r.mat_diffuse(0, 1, 0, 1, 1) +r.mat_specular(0, 0, 0, 0, 0) +r.mat_emissive(0, 0, 0, 0, 0) +r.mat_glossiness(0, 1) +r.mat_opacity(0, 1) +r.set_window_level(0, 0.25, 0.1807) +r.set_color_ramp(0, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.enable_channel(1, 1) +r.mat_diffuse(1, 1, 1, 1, 1) +r.mat_specular(1, 0, 0, 0, 0) +r.mat_emissive(1, 0, 0, 0, 0) +r.mat_glossiness(1, 1) +r.mat_opacity(1, 1) +r.set_window_level(1, 0.25, 0.2289) +r.set_color_ramp(1, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.enable_channel(2, 1) +r.mat_diffuse(2, 0, 1, 1, 1) +r.mat_specular(2, 0, 0, 0, 0) +r.mat_emissive(2, 0, 0, 0, 0) +r.mat_glossiness(2, 1) +r.mat_opacity(2, 1) +r.set_window_level(2, 0.25, 0.2289) +r.set_color_ramp(2, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.enable_channel(3, 0) +r.mat_diffuse(3, 0.500008, 0, 0, 1) +r.mat_specular(3, 0, 0, 0, 0) +r.mat_emissive(3, 0, 0, 0, 0) +r.mat_glossiness(3, 1) +r.mat_opacity(3, 1) +r.set_percentile_threshold(3, 0.5, 0.98) +r.set_color_ramp(3, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.enable_channel(4, 0) +r.mat_diffuse(4, 0, 0.145892, 0.500008, 1) +r.mat_specular(4, 0, 0, 0, 0) +r.mat_emissive(4, 0, 0, 0, 0) +r.mat_glossiness(4, 1) +r.mat_opacity(4, 1) +r.set_percentile_threshold(4, 0.5, 0.98) +r.set_color_ramp(4, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.enable_channel(5, 0) +r.mat_diffuse(5, 0.291798, 0.500008, 0, 1) +r.mat_specular(5, 0, 0, 0, 0) +r.mat_emissive(5, 0, 0, 0, 0) +r.mat_glossiness(5, 1) +r.mat_opacity(5, 1) +r.set_percentile_threshold(5, 0.5, 0.98) +r.set_color_ramp(5, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.enable_channel(6, 0) +r.mat_diffuse(6, 0.500008, 0, 0.43769, 1) +r.mat_specular(6, 0, 0, 0, 0) +r.mat_emissive(6, 0, 0, 0, 0) +r.mat_glossiness(6, 1) +r.mat_opacity(6, 1) +r.set_percentile_threshold(6, 0.5, 0.98) +r.set_color_ramp(6, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.enable_channel(7, 0) +r.mat_diffuse(7, 0, 0.500008, 0.416403, 1) +r.mat_specular(7, 0, 0, 0, 0) +r.mat_emissive(7, 0, 0, 0, 0) +r.mat_glossiness(7, 1) +r.mat_opacity(7, 1) +r.set_percentile_threshold(7, 0.5, 0.98) +r.set_color_ramp(7, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.enable_channel(8, 0) +r.mat_diffuse(8, 0.500008, 0.270512, 0, 1) +r.mat_specular(8, 0, 0, 0, 0) +r.mat_emissive(8, 0, 0, 0, 0) +r.mat_glossiness(8, 1) +r.mat_opacity(8, 1) +r.set_percentile_threshold(8, 0.5, 0.98) +r.set_color_ramp(8, "none", [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +r.skylight_top_color(1, 1, 1) +r.skylight_middle_color(1, 1, 1) +r.skylight_bottom_color(1, 1, 1) +r.light_pos(0, 1.5, 5.54884, 0.952752) +r.light_color(0, 10, 10, 10) +r.light_size(0, 0.15, 0.15) +r.session("1.zarr.png") +r.redraw() diff --git a/agave_pyvk/src/VulkanRenderer.h b/agave_pyvk/src/VulkanRenderer.h deleted file mode 100644 index 50941fd869..0000000000 --- a/agave_pyvk/src/VulkanRenderer.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include "renderlib/command.h" - -#include - -#include -#include -#include - -class CCamera; -class RenderSettings; -class Scene; - -namespace gfxApi { -class Framebuffer; -class IGestureRenderer; -class IRenderWindow; -} - -class VulkanRenderer final : public RendererCommandInterface -{ -public: - VulkanRenderer(const std::string& mode, const std::string& assetPath, int selectedGpu); - ~VulkanRenderer(); - - VulkanRenderer(const VulkanRenderer&) = delete; - VulkanRenderer& operator=(const VulkanRenderer&) = delete; - - nanobind::object execute(int commandId, const nanobind::args& args); - void close(); - - void setStreamMode(int32_t mode) override; - void resizeGL(int x, int y) override; - -private: - nanobind::object run(Command& command); - nanobind::object run(RequestRedrawCommand& command); - nanobind::object redraw(); - void initialize(const std::string& mode, const std::string& assetPath, int selectedGpu); - - int32_t m_width = 1024; - int32_t m_height = 1024; - bool m_closed = false; - bool m_ownsRuntime = false; - - std::unique_ptr m_renderSettings; - std::unique_ptr m_camera; - std::unique_ptr m_scene; - std::unique_ptr m_renderer; - std::unique_ptr m_framebuffer; - std::unique_ptr m_gestureRenderer; - ExecutionContext m_executionContext{}; -}; diff --git a/agave_pyvk/src/bindings.cpp b/agave_pyvk/src/bindings.cpp index 24344c79ba..3d119a097e 100644 --- a/agave_pyvk/src/bindings.cpp +++ b/agave_pyvk/src/bindings.cpp @@ -1,19 +1,89 @@ -#include "VulkanRenderer.h" +#include "renderlib/PythonRenderer.h" + +#include +#include +#include +#include #include #include +#include namespace nb = nanobind; +namespace { + +PythonRendererArguments +convertArguments(int commandId, const nb::args& args) +{ + const auto types = PythonRenderer::commandArgumentTypes(commandId); + if (args.size() != types.size()) { + throw PythonRendererArgumentError("command " + std::to_string(commandId) + " expects " + + std::to_string(types.size()) + " arguments, got " + std::to_string(args.size())); + } + + PythonRendererArguments converted; + converted.reserve(args.size()); + for (size_t index = 0; index < types.size(); ++index) { + switch (types[index]) { + case CommandArgType::I32: + converted.emplace_back(nb::cast(args[index])); + break; + case CommandArgType::F32: + converted.emplace_back(nb::cast(args[index])); + break; + case CommandArgType::STR: + converted.emplace_back(nb::cast(args[index])); + break; + case CommandArgType::F32A: + converted.emplace_back(nb::cast>(args[index])); + break; + case CommandArgType::I32A: + converted.emplace_back(nb::cast>(args[index])); + break; + } + } + return converted; +} + +nb::object +convertResult(PythonRendererResult result) +{ + if (std::holds_alternative(result)) { + return nb::none(); + } + if (const auto* message = std::get_if(&result)) { + return nb::str(message->c_str()); + } + + const auto& image = std::get(result); + return nb::make_tuple( + image.width, image.height, nb::bytes(reinterpret_cast(image.pixels.data()), image.pixels.size())); +} + +nb::object +execute(PythonRenderer& renderer, int commandId, const nb::args& args) +{ + try { + return convertResult(renderer.execute(commandId, convertArguments(commandId, args))); + } catch (const PythonRendererArgumentError& error) { + throw nb::type_error(error.what()); + } catch (const PythonRendererValueError& error) { + throw nb::value_error(error.what()); + } +} + +} // namespace + NB_MODULE(_native, m) { m.doc() = "Headless Vulkan bindings for AGAVE renderlib"; - nb::class_(m, "VulkanRenderer") + nb::class_(m, "PythonRenderer") .def(nb::init(), nb::arg("mode") = "pathtrace", nb::arg("asset_path") = "", nb::arg("gpu") = 0) - .def("execute", &VulkanRenderer::execute) - .def("close", &VulkanRenderer::close); + .def("execute", &execute) + .def("close", &PythonRenderer::close); } diff --git a/agave_pyvk/tests/test_api.py b/agave_pyvk/tests/test_api.py index 3e79e6d192..9b96e14203 100644 --- a/agave_pyvk/tests/test_api.py +++ b/agave_pyvk/tests/test_api.py @@ -6,7 +6,7 @@ from pathlib import Path -class FakeVulkanRenderer: +class FakePythonRenderer: def __init__(self, mode, asset_path, gpu): self.constructor_args = (mode, asset_path, gpu) self.calls = [] @@ -27,7 +27,7 @@ def close(self): def load_api(monkeypatch): fake_native = types.ModuleType("agave_pyvk._native") fake_native.__file__ = str(Path(__file__).parent / "_native.pyd") - fake_native.VulkanRenderer = FakeVulkanRenderer + fake_native.PythonRenderer = FakePythonRenderer monkeypatch.setitem(sys.modules, "agave_pyvk._native", fake_native) sys.modules.pop("agave_pyvk.agave", None) return importlib.import_module("agave_pyvk.agave") diff --git a/renderlib/CMakeLists.txt b/renderlib/CMakeLists.txt index db9cb8a753..605f660fb7 100644 --- a/renderlib/CMakeLists.txt +++ b/renderlib/CMakeLists.txt @@ -94,9 +94,11 @@ target_sources(renderlib PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/version.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/ViewerWindow.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ViewerWindow.h" - "${CMAKE_CURRENT_SOURCE_DIR}/VolumeDimensions.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/VolumeDimensions.h" -) + "${CMAKE_CURRENT_SOURCE_DIR}/VolumeDimensions.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/VolumeDimensions.h" + "${CMAKE_CURRENT_SOURCE_DIR}/PythonRenderer.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PythonRenderer.h" +) add_subdirectory(gesture) add_subdirectory(gfxapi) add_subdirectory(gfxOpenGL) diff --git a/agave_pyvk/src/VulkanRenderer.cpp b/renderlib/PythonRenderer.cpp similarity index 71% rename from agave_pyvk/src/VulkanRenderer.cpp rename to renderlib/PythonRenderer.cpp index d3d0b7ca8d..b7bda4f299 100644 --- a/agave_pyvk/src/VulkanRenderer.cpp +++ b/renderlib/PythonRenderer.cpp @@ -1,30 +1,25 @@ -#include "VulkanRenderer.h" - -#include "renderlib/AppScene.h" -#include "renderlib/BoundingBoxTool.h" -#include "renderlib/CCamera.h" -#include "renderlib/RenderSettings.h" -#include "renderlib/ScaleBarTool.h" -#include "renderlib/SceneView.h" -#include "renderlib/TimeStampTool.h" -#include "renderlib/gfxapi/Backend.h" -#include "renderlib/gfxapi/Framebuffer.h" -#include "renderlib/gfxapi/IGestureRenderer.h" -#include "renderlib/gfxapi/IRenderWindow.h" -#include "renderlib/gfxapi/RenderToFramebuffer.h" -#include "renderlib/gesture/gesture.h" -#include "renderlib/renderlib.h" - -#include -#include +#include "PythonRenderer.h" + +#include "AppScene.h" +#include "BoundingBoxTool.h" +#include "CCamera.h" +#include "RenderSettings.h" +#include "ScaleBarTool.h" +#include "SceneView.h" +#include "TimeStampTool.h" +#include "gfxapi/Backend.h" +#include "gfxapi/Framebuffer.h" +#include "gfxapi/IGestureRenderer.h" +#include "gfxapi/IRenderWindow.h" +#include "gfxapi/RenderToFramebuffer.h" +#include "gesture/gesture.h" +#include "renderlib.h" #include #include #include #include -namespace nb = nanobind; - namespace { std::mutex s_runtimeMutex; @@ -32,23 +27,26 @@ int s_runtimeUsers = 0; template T -arg(const nb::args& args, size_t index) +arg(const PythonRendererArguments& args, size_t index) { - return nb::cast(args[index]); + try { + return std::get(args[index]); + } catch (const std::bad_variant_access&) { + throw PythonRendererArgumentError("command argument " + std::to_string(index) + " has the wrong type"); + } } void -requireArgs(int commandId, const nb::args& args, size_t expected) +requireArgs(int commandId, const PythonRendererArguments& args, size_t expected) { if (args.size() != expected) { - throw nb::type_error(("command " + std::to_string(commandId) + " expects " + std::to_string(expected) + - " arguments, got " + std::to_string(args.size())) - .c_str()); + throw PythonRendererArgumentError("command " + std::to_string(commandId) + " expects " + std::to_string(expected) + + " arguments, got " + std::to_string(args.size())); } } LoadDataCommandD -makeLoadDataCommandData(const nb::args& args) +makeLoadDataCommandData(const PythonRendererArguments& args) { const auto region = arg>(args, 5); LoadDataCommandD data{ arg(args, 0), @@ -70,25 +68,25 @@ makeLoadDataCommandData(const nb::args& args) data.m_zmin = region[4]; data.m_zmax = region[5]; } else if (!region.empty()) { - throw nb::value_error("LOAD_DATA region must be empty or contain six integers"); + throw PythonRendererValueError("LOAD_DATA region must be empty or contain six integers"); } return data; } } // namespace -VulkanRenderer::VulkanRenderer(const std::string& mode, const std::string& assetPath, int selectedGpu) +PythonRenderer::PythonRenderer(const std::string& mode, const std::string& assetPath, int selectedGpu) { initialize(mode, assetPath, selectedGpu); } -VulkanRenderer::~VulkanRenderer() +PythonRenderer::~PythonRenderer() { close(); } void -VulkanRenderer::initialize(const std::string& mode, const std::string& assetPath, int selectedGpu) +PythonRenderer::initialize(const std::string& mode, const std::string& assetPath, int selectedGpu) { std::lock_guard lock(s_runtimeMutex); if (s_runtimeUsers == 0) { @@ -147,7 +145,7 @@ VulkanRenderer::initialize(const std::string& mode, const std::string& assetPath } void -VulkanRenderer::close() +PythonRenderer::close() { if (m_closed) { return; @@ -172,13 +170,13 @@ VulkanRenderer::close() } void -VulkanRenderer::setStreamMode(int32_t) +PythonRenderer::setStreamMode(int32_t) { // Synchronous in-process rendering has no streaming mode. } void -VulkanRenderer::resizeGL(int x, int y) +PythonRenderer::resizeGL(int x, int y) { m_width = x; m_height = y; @@ -187,8 +185,8 @@ VulkanRenderer::resizeGL(int x, int y) { static_cast(x), static_cast(y), gfxApi::FramebufferColorFormat::Rgba8, true }); } -nb::object -VulkanRenderer::run(Command& command) +PythonRendererResult +PythonRenderer::run(Command& command) { if (m_closed) { throw std::runtime_error("Renderer is closed"); @@ -196,20 +194,20 @@ VulkanRenderer::run(Command& command) m_executionContext.m_message.clear(); command.execute(&m_executionContext); if (!m_executionContext.m_message.empty()) { - return nb::str(m_executionContext.m_message.c_str()); + return m_executionContext.m_message; } - return nb::none(); + return std::monostate{}; } -nb::object -VulkanRenderer::run(RequestRedrawCommand& command) +PythonRendererResult +PythonRenderer::run(RequestRedrawCommand& command) { run(static_cast(command)); return redraw(); } -nb::object -VulkanRenderer::redraw() +PythonRendererResult +PythonRenderer::redraw() { m_camera->Update(); @@ -233,13 +231,84 @@ VulkanRenderer::redraw() gfxApi::renderToFramebuffer(*m_framebuffer, *m_renderer, *m_gestureRenderer, sceneView, gesture.graphics, 0.0f); - std::vector pixels(static_cast(m_width) * static_cast(m_height) * 4); - m_framebuffer->toImage(pixels.data()); - return nb::make_tuple(m_width, m_height, nb::bytes(reinterpret_cast(pixels.data()), pixels.size())); + PythonRendererImage image{ m_width, + m_height, + std::vector(static_cast(m_width) * static_cast(m_height) * 4) }; + m_framebuffer->toImage(image.pixels.data()); + return image; +} + +std::vector +PythonRenderer::commandArgumentTypes(int commandId) +{ +#define COMMAND_ARGUMENT_TYPES(ID, TYPE) \ + case ID: \ + return TYPE::ArgTypes() + + switch (commandId) { + COMMAND_ARGUMENT_TYPES(0, SessionCommand); + COMMAND_ARGUMENT_TYPES(1, AssetPathCommand); + COMMAND_ARGUMENT_TYPES(2, LoadOmeTifCommand); + COMMAND_ARGUMENT_TYPES(3, SetCameraPosCommand); + COMMAND_ARGUMENT_TYPES(4, SetCameraTargetCommand); + COMMAND_ARGUMENT_TYPES(5, SetCameraUpCommand); + COMMAND_ARGUMENT_TYPES(6, SetCameraApertureCommand); + COMMAND_ARGUMENT_TYPES(7, SetCameraProjectionCommand); + COMMAND_ARGUMENT_TYPES(8, SetCameraFocalDistanceCommand); + COMMAND_ARGUMENT_TYPES(9, SetCameraExposureCommand); + COMMAND_ARGUMENT_TYPES(10, SetDiffuseColorCommand); + COMMAND_ARGUMENT_TYPES(11, SetSpecularColorCommand); + COMMAND_ARGUMENT_TYPES(12, SetEmissiveColorCommand); + COMMAND_ARGUMENT_TYPES(13, SetRenderIterationsCommand); + COMMAND_ARGUMENT_TYPES(14, SetStreamModeCommand); + COMMAND_ARGUMENT_TYPES(15, RequestRedrawCommand); + COMMAND_ARGUMENT_TYPES(16, SetResolutionCommand); + COMMAND_ARGUMENT_TYPES(17, SetDensityCommand); + COMMAND_ARGUMENT_TYPES(18, FrameSceneCommand); + COMMAND_ARGUMENT_TYPES(19, SetGlossinessCommand); + COMMAND_ARGUMENT_TYPES(20, EnableChannelCommand); + COMMAND_ARGUMENT_TYPES(21, SetWindowLevelCommand); + COMMAND_ARGUMENT_TYPES(22, OrbitCameraCommand); + COMMAND_ARGUMENT_TYPES(23, SetSkylightTopColorCommand); + COMMAND_ARGUMENT_TYPES(24, SetSkylightMiddleColorCommand); + COMMAND_ARGUMENT_TYPES(25, SetSkylightBottomColorCommand); + COMMAND_ARGUMENT_TYPES(26, SetLightPosCommand); + COMMAND_ARGUMENT_TYPES(27, SetLightColorCommand); + COMMAND_ARGUMENT_TYPES(28, SetLightSizeCommand); + COMMAND_ARGUMENT_TYPES(29, SetClipRegionCommand); + COMMAND_ARGUMENT_TYPES(30, SetVoxelScaleCommand); + COMMAND_ARGUMENT_TYPES(31, AutoThresholdCommand); + COMMAND_ARGUMENT_TYPES(32, SetPercentileThresholdCommand); + COMMAND_ARGUMENT_TYPES(33, SetOpacityCommand); + COMMAND_ARGUMENT_TYPES(34, SetPrimaryRayStepSizeCommand); + COMMAND_ARGUMENT_TYPES(35, SetSecondaryRayStepSizeCommand); + COMMAND_ARGUMENT_TYPES(36, SetBackgroundColorCommand); + COMMAND_ARGUMENT_TYPES(37, SetIsovalueThresholdCommand); + COMMAND_ARGUMENT_TYPES(38, SetControlPointsCommand); + COMMAND_ARGUMENT_TYPES(39, LoadVolumeFromFileCommand); + COMMAND_ARGUMENT_TYPES(40, SetTimeCommand); + COMMAND_ARGUMENT_TYPES(41, SetBoundingBoxColorCommand); + COMMAND_ARGUMENT_TYPES(42, ShowBoundingBoxCommand); + COMMAND_ARGUMENT_TYPES(43, TrackballCameraCommand); + COMMAND_ARGUMENT_TYPES(44, LoadDataCommand); + COMMAND_ARGUMENT_TYPES(45, ShowScaleBarCommand); + COMMAND_ARGUMENT_TYPES(46, SetFlipAxisCommand); + COMMAND_ARGUMENT_TYPES(47, SetInterpolationCommand); + COMMAND_ARGUMENT_TYPES(48, SetClipPlaneCommand); + COMMAND_ARGUMENT_TYPES(49, SetColorRampCommand); + COMMAND_ARGUMENT_TYPES(50, SetMinMaxThresholdCommand); + COMMAND_ARGUMENT_TYPES(51, SetSkylightRotationCommand); + COMMAND_ARGUMENT_TYPES(52, ShowTimeStampCommand); + COMMAND_ARGUMENT_TYPES(53, SetTimeStampFormatCommand); + default: + throw PythonRendererValueError("Unknown AGAVE command id " + std::to_string(commandId)); + } + +#undef COMMAND_ARGUMENT_TYPES } -nb::object -VulkanRenderer::execute(int commandId, const nb::args& args) +PythonRendererResult +PythonRenderer::execute(int commandId, const PythonRendererArguments& args) { #define EXECUTE_COMMAND(ID, COUNT, TYPE, ...) \ case ID: { \ @@ -396,7 +465,7 @@ VulkanRenderer::execute(int commandId, const nb::args& args) EXECUTE_COMMAND(52, 1, ShowTimeStampCommand, ShowTimeStampCommandD{ arg(args, 0) }); EXECUTE_COMMAND(53, 1, SetTimeStampFormatCommand, SetTimeStampFormatCommandD{ arg(args, 0) }); default: - throw nb::value_error(("Unknown AGAVE command id " + std::to_string(commandId)).c_str()); + throw PythonRendererValueError("Unknown AGAVE command id " + std::to_string(commandId)); } #undef EXECUTE_COMMAND diff --git a/renderlib/PythonRenderer.h b/renderlib/PythonRenderer.h new file mode 100644 index 0000000000..fcac54b6c5 --- /dev/null +++ b/renderlib/PythonRenderer.h @@ -0,0 +1,81 @@ +#pragma once + +#include "command.h" + +#include +#include +#include +#include +#include +#include + +class CCamera; +class RenderSettings; +class Scene; + +namespace gfxApi { +class Framebuffer; +class IGestureRenderer; +class IRenderWindow; +} + +using PythonRendererArgument = std::variant, std::vector>; +using PythonRendererArguments = std::vector; + +struct PythonRendererImage +{ + int32_t width; + int32_t height; + std::vector pixels; +}; + +using PythonRendererResult = std::variant; + +class PythonRendererArgumentError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +class PythonRendererValueError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +// Purpose-built synchronous renderer for the Python wrapper around renderlib. +class PythonRenderer final : public RendererCommandInterface +{ +public: + PythonRenderer(const std::string& mode, const std::string& assetPath, int selectedGpu); + ~PythonRenderer(); + + PythonRenderer(const PythonRenderer&) = delete; + PythonRenderer& operator=(const PythonRenderer&) = delete; + + static std::vector commandArgumentTypes(int commandId); + PythonRendererResult execute(int commandId, const PythonRendererArguments& args); + void close(); + + void setStreamMode(int32_t mode) override; + void resizeGL(int x, int y) override; + +private: + PythonRendererResult run(Command& command); + PythonRendererResult run(RequestRedrawCommand& command); + PythonRendererResult redraw(); + void initialize(const std::string& mode, const std::string& assetPath, int selectedGpu); + + int32_t m_width = 1024; + int32_t m_height = 1024; + bool m_closed = false; + bool m_ownsRuntime = false; + + std::unique_ptr m_renderSettings; + std::unique_ptr m_camera; + std::unique_ptr m_scene; + std::unique_ptr m_renderer; + std::unique_ptr m_framebuffer; + std::unique_ptr m_gestureRenderer; + ExecutionContext m_executionContext{}; +}; From ad111a4ca55b4c9deabea36364842ec3c63cc098 Mon Sep 17 00:00:00 2001 From: toloudis Date: Sun, 16 Aug 2026 14:31:59 -0700 Subject: [PATCH 35/35] build fixup --- agave_app/renderDialog.cpp | 4 ++-- renderlib/CacheManager.h | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/agave_app/renderDialog.cpp b/agave_app/renderDialog.cpp index 2378c9fa52..0aef12f5cc 100644 --- a/agave_app/renderDialog.cpp +++ b/agave_app/renderDialog.cpp @@ -246,7 +246,7 @@ RenderDialog::RenderDialog(ViewerWindow* borrowedRenderer, , m_renderSettings(renderSettings) , m_scene(scene) , m_camera(ccamera) - , m_glContext(std::make_unique(glContext)) + , m_glContext(glContext) , m_loadSpec(loadSpec) , m_renderThread(nullptr) , m_frameRenderTime(0) @@ -767,7 +767,7 @@ RenderDialog::render() m_camera, m_loadSpec, renderlib::RendererType_Pathtrace, - m_glContext.get(), + m_glContext, mCaptureSettings); onZoomFitClicked(); diff --git a/renderlib/CacheManager.h b/renderlib/CacheManager.h index a8f54f70ee..3d4fff027b 100644 --- a/renderlib/CacheManager.h +++ b/renderlib/CacheManager.h @@ -102,11 +102,6 @@ class CacheManager // Precondition: caller must hold m_mutex. void evictIfNeededLocked(std::uint64_t incomingBytes); void storeImageInMemory(const CacheKey& key, const std::shared_ptr& image); - // Writes a marker file to a directory we manage as our own disk cache root. - // clearDiskCache refuses to delete anything unless this marker is present, - // protecting against accidental wipes of user-typed paths (e.g. "C:\"). - void writeCacheMarker(const std::string& path) const; - bool isAgaveCacheDir(const std::string& path) const; std::shared_ptr loadFromDisk(const CacheKey& key, const CacheConfig& config, const std::string& cacheDir); void storeToDisk(const CacheKey& key,