diff --git a/.github/workflows/contrib_checks.yml b/.github/workflows/contrib_checks.yml index d93730a7db40..967edbacb36b 100644 --- a/.github/workflows/contrib_checks.yml +++ b/.github/workflows/contrib_checks.yml @@ -31,6 +31,9 @@ env: # these incremental artifacts when running on CI. CARGO_INCREMENTAL: "0" + # Sourced from https://vulkan.lunarg.com/sdk/home#linux + VULKAN_SDK_VERSION: "1.3.296.0" + defaults: run: shell: bash @@ -97,6 +100,19 @@ jobs: with: pixi-version: v0.39.0 + # Install the Vulkan SDK, so we can use the software rasterizer. + # TODO(andreas): It would be nice if `setup_software_rasterizer.py` could do that for us as well (note though that this action here is very fast when cached!) + - name: Install Vulkan SDK + uses: jakoch/install-vulkan-sdk-action@v1.0.5 + with: + vulkan_version: ${{ env.VULKAN_SDK_VERSION }} + install_runtime: true + cache: false + stripdown: true + + - name: Setup software rasterizer + run: pixi run python ./scripts/ci/setup_software_rasterizer.py + - name: Rust checks & tests run: pixi run rs-check --skip individual_crates docs_slow diff --git a/.github/workflows/reusable_checks_rust.yml b/.github/workflows/reusable_checks_rust.yml index b245aeabe635..4f1d4f2fe869 100644 --- a/.github/workflows/reusable_checks_rust.yml +++ b/.github/workflows/reusable_checks_rust.yml @@ -15,7 +15,6 @@ concurrency: cancel-in-progress: true env: - PYTHON_VERSION: "3.8" # web_sys_unstable_apis is required to enable the web_sys clipboard API which egui_web uses # https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html # https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html @@ -34,6 +33,12 @@ env: # these incremental artifacts when running on CI. CARGO_INCREMENTAL: "0" + # Improve diagnostics for crashes. + RUST_BACKTRACE: full + + # Sourced from https://vulkan.lunarg.com/sdk/home#linux + VULKAN_SDK_VERSION: "1.3.296.0" + defaults: run: shell: bash @@ -52,6 +57,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} + lfs: true - name: Set up Rust uses: ./.github/actions/setup-rust @@ -65,11 +71,32 @@ jobs: with: pixi-version: v0.39.0 - - name: Rust checks & tests + # Install the Vulkan SDK, so we can use the software rasterizer. + # TODO(andreas): It would be nice if `setup_software_rasterizer.py` could do that for us as well (note though that this action here is very fast when cached!) + - name: Install Vulkan SDK + uses: jakoch/install-vulkan-sdk-action@v1.0.5 + with: + vulkan_version: ${{ env.VULKAN_SDK_VERSION }} + install_runtime: true + cache: false + stripdown: true + + - name: Setup software rasterizer + run: pixi run python ./scripts/ci/setup_software_rasterizer.py + + - name: Rust checks (PR subset) if: ${{ inputs.CHANNEL == 'pr' }} - run: pixi run rs-check --skip individual_crates tests docs_slow + run: pixi run rs-check --only base_checks sdk_variations cargo_deny wasm docs - - name: Rust checks & tests + - name: Download test assets + run: pixi run python ./tests/assets/download_test_assets.py + + - name: Run tests (`cargo test --all-targets --all-features`) + if: ${{ inputs.CHANNEL == 'pr' }} + # Need to use pixi due to NASM dependency. + run: pixi run cargo test --all-targets --all-features + + - name: Rust most checks & tests if: ${{ inputs.CHANNEL == 'main' }} run: pixi run rs-check --skip individual_crates docs_slow @@ -83,10 +110,10 @@ jobs: strategy: matrix: include: - # TODO(#8245): we run mac tests on `main` because that's the only platform where UI snapshot tests are covered. - # When the linux runners are able to run these tests (with a software renderer), we can move that back to all nightly. - - os: ${{ inputs.CHANNEL == 'main' && 'macos-latest' || 'windows-latest-8-cores' }} - name: ${{ inputs.CHANNEL == 'main' && 'macos' || 'windows' }} + - os: "macos-latest" + name: "macos" + - os: "windows-latest-8-cores" + name: "windows" # Note: we can't use `matrix.os` here because its evaluated before the matrix stuff. if: ${{ inputs.CHANNEL == 'main' || inputs.CHANNEL == 'nightly' }} @@ -109,8 +136,28 @@ jobs: with: pixi-version: v0.39.0 + # Install the Vulkan SDK, so we can use the software rasterizer. + # TODO(andreas): It would be nice if `setup_software_rasterizer.py` could do that for us as well (note though that this action here is very fast when cached!) + - name: Install Vulkan SDK + if: ${{ matrix.name != 'macos' }} + uses: jakoch/install-vulkan-sdk-action@v1.0.5 + with: + vulkan_version: ${{ env.VULKAN_SDK_VERSION }} + install_runtime: true + cache: true + stripdown: true + + - name: Setup software rasterizer + run: pixi run python ./scripts/ci/setup_software_rasterizer.py + - name: Download test assets run: pixi run python ./tests/assets/download_test_assets.py - - name: pixi run cargo test --all-targets --all-features + - name: Run tests (`cargo test --all-targets --all-features`) + if: ${{ inputs.CHANNEL != 'nightly' }} + # Need to use pixi due to NASM dependency. run: pixi run cargo test --all-targets --all-features + + - name: Rust all checks & tests + if: ${{ inputs.CHANNEL == 'nightly' }} + run: pixi run rs-check diff --git a/.gitignore b/.gitignore index 43b4208ba656..112556bde879 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,12 @@ wheels **/tests/snapshots/**/*.diff.png **/tests/snapshots/**/*.new.png +# Mesa install +mesa +mesa.7z +mesa.tar.xz +icd.json + *.rrd /meilisearch diff --git a/crates/viewer/re_component_ui/tests/test_all_components_ui.rs b/crates/viewer/re_component_ui/tests/test_all_components_ui.rs index 1c966332a2a7..a67a8d835336 100644 --- a/crates/viewer/re_component_ui/tests/test_all_components_ui.rs +++ b/crates/viewer/re_component_ui/tests/test_all_components_ui.rs @@ -227,13 +227,7 @@ fn test_single_component_ui_as_list_item( }); harness.run(); - - //TODO(#8245): enable this everywhere when we have a software renderer setup - if cfg!(target_os = "macos") { - harness.try_snapshot_options(&format!("{test_case}"), _snapshot_options) - } else { - Ok(()) - } + harness.try_snapshot_options(&format!("{test_case}"), _snapshot_options) } // --- diff --git a/crates/viewer/re_renderer/src/context.rs b/crates/viewer/re_renderer/src/context.rs index 191b8d6174f3..5e50341e77c0 100644 --- a/crates/viewer/re_renderer/src/context.rs +++ b/crates/viewer/re_renderer/src/context.rs @@ -173,6 +173,7 @@ impl RenderContext { before_view_builder_encoder: Mutex::new(FrameGlobalCommandEncoder::new(&device)), frame_index: STARTUP_FRAME_IDX, top_level_error_scope, + num_view_builders_created: AtomicU64::new(0), }; // Register shader workarounds for the current device. @@ -316,6 +317,7 @@ This means, either a call to RenderContext::before_submit was omitted, or the pr before_view_builder_encoder: Mutex::new(FrameGlobalCommandEncoder::new(&self.device)), frame_index: self.active_frame.frame_index.wrapping_add(1), top_level_error_scope: Some(WgpuErrorScope::start(&self.device)), + num_view_builders_created: AtomicU64::new(0), }; let frame_index = self.active_frame.frame_index; @@ -485,6 +487,9 @@ pub struct ActiveFrameContext { /// /// The only time this is allowed to be `None` is during shutdown and when closing an old and opening a new scope. top_level_error_scope: Option, + + /// Number of view builders created in this frame so far. + pub num_view_builders_created: AtomicU64, } fn log_adapter_info(info: &wgpu::AdapterInfo) { diff --git a/crates/viewer/re_renderer/src/view_builder.rs b/crates/viewer/re_renderer/src/view_builder.rs index 2455595e7e66..efe1d8b58d01 100644 --- a/crates/viewer/re_renderer/src/view_builder.rs +++ b/crates/viewer/re_renderer/src/view_builder.rs @@ -518,6 +518,10 @@ impl ViewBuilder { frame_uniform_buffer_content, }; + ctx.active_frame + .num_view_builders_created + .fetch_add(1, std::sync::atomic::Ordering::Release); + Self { setup, queued_draws: vec![composition_draw.into()], diff --git a/crates/viewer/re_time_panel/tests/time_panel_tests.rs b/crates/viewer/re_time_panel/tests/time_panel_tests.rs index ae93eac6531e..15e7d5dfd754 100644 --- a/crates/viewer/re_time_panel/tests/time_panel_tests.rs +++ b/crates/viewer/re_time_panel/tests/time_panel_tests.rs @@ -103,8 +103,5 @@ fn run_time_panel_and_save_snapshot(mut test_context: TestContext, _snapshot_nam }); harness.run(); - - //TODO(#8245): enable this everywhere when we have a software renderer setup - #[cfg(target_os = "macos")] harness.snapshot(_snapshot_name); } diff --git a/crates/viewer/re_ui/tests/arrow_ui_test.rs b/crates/viewer/re_ui/tests/arrow_ui_test.rs index 3e84288104b9..d3ed71595fec 100644 --- a/crates/viewer/re_ui/tests/arrow_ui_test.rs +++ b/crates/viewer/re_ui/tests/arrow_ui_test.rs @@ -17,9 +17,6 @@ pub fn test_arrow_ui() { harness.fit_contents(); harness.run(); - - //TODO(#8245): enable this everywhere when we have a software renderer setup - #[cfg(target_os = "macos")] harness.snapshot("arrow_ui"); } diff --git a/crates/viewer/re_ui/tests/list_item_tests.rs b/crates/viewer/re_ui/tests/list_item_tests.rs index dcabc9d28a0d..86db31082935 100644 --- a/crates/viewer/re_ui/tests/list_item_tests.rs +++ b/crates/viewer/re_ui/tests/list_item_tests.rs @@ -200,8 +200,5 @@ pub fn test_list_items_should_match_snapshot() { }); harness.run(); - - //TODO(#8245): enable this everywhere when we have a software renderer setup - #[cfg(target_os = "macos")] harness.snapshot("list_items"); } diff --git a/crates/viewer/re_ui/tests/modal_tests.rs b/crates/viewer/re_ui/tests/modal_tests.rs index 75f5f4b61068..42e8c58b6f6c 100644 --- a/crates/viewer/re_ui/tests/modal_tests.rs +++ b/crates/viewer/re_ui/tests/modal_tests.rs @@ -61,8 +61,5 @@ fn run_modal_test( }); harness.run(); - - //TODO(#8245): enable this everywhere when we have a software renderer setup - #[cfg(target_os = "macos")] harness.snapshot(_test_name); } diff --git a/crates/viewer/re_view_graph/tests/basic.rs b/crates/viewer/re_view_graph/tests/basic.rs index a394fe5b6c27..4701354e814c 100644 --- a/crates/viewer/re_view_graph/tests/basic.rs +++ b/crates/viewer/re_view_graph/tests/basic.rs @@ -247,9 +247,6 @@ fn run_graph_view_and_save_snapshot( }); harness.run(); - - //TODO(#8245): enable this everywhere when we have a software renderer setup - #[cfg(target_os = "macos")] harness.snapshot(_name); Ok(()) diff --git a/crates/viewer/re_viewer_context/src/test_context.rs b/crates/viewer/re_viewer_context/src/test_context.rs index cd9354ab1adb..8b0b5c74d6ee 100644 --- a/crates/viewer/re_viewer_context/src/test_context.rs +++ b/crates/viewer/re_viewer_context/src/test_context.rs @@ -1,3 +1,4 @@ +use std::sync::atomic::AtomicBool; use std::sync::Arc; use ahash::HashMap; @@ -6,7 +7,6 @@ use parking_lot::Mutex; use re_chunk_store::LatestAtQuery; use re_entity_db::EntityDb; -use re_log::ResultExt as _; use re_log_types::{StoreId, StoreKind}; use re_types_core::reflection::Reflection; @@ -44,7 +44,9 @@ pub struct TestContext { command_sender: CommandSender, command_receiver: CommandReceiver, + egui_render_state: Mutex>, + called_setup_kittest_for_rendering: AtomicBool, } impl Default for TestContext { @@ -84,17 +86,16 @@ impl Default for TestContext { // Created lazily since each egui_kittest harness needs a new one. egui_render_state: Mutex::new(None), + called_setup_kittest_for_rendering: AtomicBool::new(false), } } } /// Create an `egui_wgpu::RenderState` for tests. -/// -/// May be `None` if we failed to initialize the wgpu renderer setup. -fn create_egui_renderstate() -> Option { +fn create_egui_renderstate() -> egui_wgpu::RenderState { re_tracing::profile_function!(); - let shared_wgpu_setup = (*SHARED_WGPU_RENDERER_SETUP).as_ref()?; + let shared_wgpu_setup = &*SHARED_WGPU_RENDERER_SETUP; let config = egui_wgpu::WgpuConfiguration { wgpu_setup: egui_wgpu::WgpuSetupExisting { @@ -142,7 +143,8 @@ fn create_egui_renderstate() -> Option { ) .expect("Failed to initialize re_renderer"), ); - Some(render_state) + + render_state } /// Instance & adapter @@ -156,54 +158,93 @@ struct SharedWgpuResources { queue: Arc, } -static SHARED_WGPU_RENDERER_SETUP: Lazy> = - Lazy::new(try_init_shared_renderer_setup); +static SHARED_WGPU_RENDERER_SETUP: Lazy = + Lazy::new(init_shared_renderer_setup); -fn try_init_shared_renderer_setup() -> Option { +fn init_shared_renderer_setup() -> SharedWgpuResources { // TODO(andreas, emilk/egui#5506): Use centralized wgpu setup logic that… // * lives mostly in re_renderer and is shared with viewer & renderer examples // * can be told to prefer software rendering // * can be told to match a specific device tier - // For the moment we just use wgpu defaults. - // TODO(#8245): Should we require this to succeed? + // We rely a lot on logging in the viewer to identify issues. + // Make sure logging is set up if it hasn't been done yet. + //let _ = env_logger::builder().is_test(true).try_init(); + let _ = env_logger::builder() + .filter_level(re_log::external::log::LevelFilter::Trace) + .is_test(false) + .try_init(); + + // We don't test on GL & DX12 right now (and don't want to do so by mistake!). + let backends = wgpu::Backends::VULKAN | wgpu::Backends::METAL; + let flags = (wgpu::InstanceFlags::ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER + | wgpu::InstanceFlags::VALIDATION + | wgpu::InstanceFlags::GPU_BASED_VALIDATION) + .with_env(); // Allow overwriting flags via env vars. + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + flags, + ..Default::default() + }); + + let mut adapters = instance.enumerate_adapters(backends); + assert!(!adapters.is_empty(), "No graphics adapter found!"); + re_log::info!("Found the following adapters:"); + for adapter in &adapters { + re_log::info!("* {}", egui_wgpu::adapter_info_summary(&adapter.get_info())); + } - let instance = wgpu::Instance::default(); - let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - force_fallback_adapter: false, - compatible_surface: None, - }))?; + // Adapters are already sorted by preferred backend by wgpu, but let's be explicit. + adapters.sort_by_key(|a| match a.get_info().backend { + wgpu::Backend::Metal => 0, + wgpu::Backend::Vulkan => 1, + wgpu::Backend::Dx12 => 2, + wgpu::Backend::Gl => 4, + wgpu::Backend::BrowserWebGpu => 6, + wgpu::Backend::Empty => 7, + }); + + // Prefer CPU adapters, otherwise if we can't, prefer discrete GPU over integrated GPU. + adapters.sort_by_key(|a| match a.get_info().device_type { + wgpu::DeviceType::Cpu => 0, // CPU is the best for our purposes! + wgpu::DeviceType::DiscreteGpu => 1, + wgpu::DeviceType::Other + | wgpu::DeviceType::IntegratedGpu + | wgpu::DeviceType::VirtualGpu => 2, + }); + + let adapter = adapters.remove(0); + re_log::info!("Picked adapter: {:?}", adapter.get_info()); let device_caps = re_renderer::config::DeviceCaps::from_adapter(&adapter) - .warn_on_err_once("Failed to determine device capabilities")?; + .expect("Failed to determine device capabilities"); let (device, queue) = pollster::block_on(adapter.request_device(&device_caps.device_descriptor(), None)) - .warn_on_err_once("Failed to request device.")?; + .expect("Failed to request device."); - Some(SharedWgpuResources { + SharedWgpuResources { instance: Arc::new(instance), adapter: Arc::new(adapter), device: Arc::new(device), queue: Arc::new(queue), - }) + } } impl TestContext { pub fn setup_kittest_for_rendering(&self) -> egui_kittest::HarnessBuilder<()> { - if let Some(new_render_state) = create_egui_renderstate() { - let builder = egui_kittest::Harness::builder().renderer( - // Note that render state clone is mostly cloning of inner `Arc`. - // This does _not_ duplicate re_renderer's context. - egui_kittest::wgpu::WgpuTestRenderer::from_render_state(new_render_state.clone()), - ); - - // Egui kittests insists on having a fresh render state for each test. - self.egui_render_state.lock().replace(new_render_state); - builder - } else { - egui_kittest::Harness::builder() - } + // Egui kittests insists on having a fresh render state for each test. + let new_render_state = create_egui_renderstate(); + let builder = egui_kittest::Harness::builder().renderer( + // Note that render state clone is mostly cloning of inner `Arc`. + // This does _not_ duplicate re_renderer's context contained within. + egui_kittest::wgpu::WgpuTestRenderer::from_render_state(new_render_state.clone()), + ); + self.egui_render_state.lock().replace(new_render_state); + + self.called_setup_kittest_for_rendering + .store(true, std::sync::atomic::Ordering::Relaxed); + + builder } /// Timeline the recording config is using by default. @@ -237,19 +278,14 @@ impl TestContext { let drag_and_drop_manager = crate::DragAndDropManager::new(ItemCollection::default()); - let context_render_state = self.egui_render_state.lock(); - let mut renderer; - let render_ctx = if let Some(render_state) = context_render_state.as_ref() { - renderer = render_state.renderer.write(); - let render_ctx = renderer - .callback_resources - .get_mut::() - .expect("No re_renderer::RenderContext in egui_render_state"); - render_ctx.begin_frame(); - Some(render_ctx) - } else { - None - }; + let mut context_render_state = self.egui_render_state.lock(); + let render_state = context_render_state.get_or_insert_with(create_egui_renderstate); + let mut egui_renderer = render_state.renderer.write(); + let render_ctx = egui_renderer + .callback_resources + .get_mut::() + .expect("No re_renderer::RenderContext in egui_render_state"); + render_ctx.begin_frame(); let ctx = ViewerContext { app_options: &Default::default(), @@ -266,7 +302,7 @@ impl TestContext { selection_state: &self.selection_state, blueprint_query: &self.blueprint_query, egui_ctx, - render_ctx: render_ctx.as_deref(), + render_ctx: Some(render_ctx), command_sender: &self.command_sender, focused_item: &None, drag_and_drop_manager: &drag_and_drop_manager, @@ -274,9 +310,22 @@ impl TestContext { func(&ctx); - if let Some(render_ctx) = render_ctx { - render_ctx.before_submit(); + // If re_renderer was used, `setup_kittest_for_rendering` should have been called. + // (if not, then we clearly didn't render anything since that requires writing to a texture!). + if render_ctx + .active_frame + .num_view_builders_created + .load(std::sync::atomic::Ordering::Acquire) + != 0 + && !self + .called_setup_kittest_for_rendering + .load(std::sync::atomic::Ordering::Relaxed) + { + panic!("Rendering with `re_renderer` requires setting up kittest with `TestContext::setup_kittest_for_rendering` + to ensure that kittest & re_renderer use the same graphics device."); } + + render_ctx.before_submit(); } /// Run the given function with a [`ViewerContext`] produced by the [`Self`], in the context of diff --git a/scripts/ci/setup_software_rasterizer.py b/scripts/ci/setup_software_rasterizer.py new file mode 100644 index 000000000000..850528a3eb3b --- /dev/null +++ b/scripts/ci/setup_software_rasterizer.py @@ -0,0 +1,225 @@ +""" +Sets up software rasterizers for CI. + +Borrows heavily from wgpu's CI setup. +See https://github.com/gfx-rs/wgpu/blob/a8a91737b2d2f378976e292074c75817593a0224/.github/workflows/ci.yml#L10 +In fact we're the exact same Mesa builds that wgpu produces, +see https://github.com/gfx-rs/ci-build +""" + +from __future__ import annotations + +import os +import platform +import shutil +import subprocess +import sys +from distutils.dir_util import copy_tree +from pathlib import Path + +# Sourced from https://archive.mesa3d.org/. Bumping this requires +# updating the mesa build in https://github.com/gfx-rs/ci-build and creating a new release. +MESA_VERSION = "24.2.3" + +# Corresponds to https://github.com/gfx-rs/ci-build/releases +CI_BINARY_BUILD = "build19" + +TARGET_DIR = Path("target/debug") + + +def run( + args: list[str], *, env: dict[str, str] | None = None, timeout: int | None = None, cwd: str | None = None +) -> subprocess.CompletedProcess[str]: + result = subprocess.run(args, env=env, cwd=cwd, timeout=timeout, check=False, capture_output=True, text=True) + assert ( + result.returncode == 0 + ), f"{subprocess.list2cmdline(args)} failed with exit-code {result.returncode}. Output:\n{result.stdout}\n{result.stderr}" + return result + + +def set_environment_variables(variables: dict[str, str]) -> None: + """ + Sets environment variables in the GITHUB_ENV file. + + If `GITHUB_ENV` is not set (i.e. when running locally), prints the variables to stdout. + """ + for key, value in variables.items(): + os.environ[key] = value + + # Set in GITHUB_ENV file. + github_env = os.environ.get("GITHUB_ENV") + if github_env is None: + print(f"GITHUB_ENV is not set. The following environment variables need to be set:\n{variables}") + else: + print(f"Setting environment variables in {github_env}:\n{variables}") + # Write to GITHUB_ENV file. + with open(github_env, "a", encoding="utf-8") as f: + for key, value in variables.items(): + f.write(f"{key}={value}\n") + + +def setup_lavapipe_for_linux() -> dict[str, str]: + """Sets up lavapipe mesa driver for Linux (x64).""" + # Download mesa + run([ + "curl", + "-L", + "--retry", + "5", + f"https://github.com/gfx-rs/ci-build/releases/download/{CI_BINARY_BUILD}/mesa-{MESA_VERSION}-linux-x86_64.tar.xz", + "-o", + "mesa.tar.xz", + ]) + + # Create mesa directory and extract + os.makedirs("mesa", exist_ok=True) + run(["tar", "xpf", "mesa.tar.xz", "-C", "mesa"]) + + # The ICD provided by the mesa build is hardcoded to the build environment. + # We write out our own ICD file to point to the mesa vulkan + icd_json = f"""{{ + "ICD": {{ + "api_version": "1.1.255", + "library_path": "{os.getcwd()}/mesa/lib/x86_64-linux-gnu/libvulkan_lvp.so" + }}, + "file_format_version": "1.0.0" +}}""" + icd_json_path = Path("icd.json") + with open(icd_json_path, "w", encoding="utf-8") as f: + f.write(icd_json) + + # Update environment variables + env_vars = { + "VK_DRIVER_FILES": f"{os.getcwd()}/{icd_json_path}", + "LD_LIBRARY_PATH": f"{os.getcwd()}/mesa/lib/x86_64-linux-gnu/:{os.environ.get('LD_LIBRARY_PATH', '')}", + } + set_environment_variables(env_vars) + + # On CI we run with elevated privileges, therefore VK_DRIVER_FILES is ignored. + # See: https://github.com/KhronosGroup/Vulkan-Loader/blob/sdk-1.3.261/docs/LoaderInterfaceArchitecture.md#elevated-privilege-caveats + # (curiously, when installing the Vulkan SDK via apt, it seems to work fine). + # Therefore, we copy the icd file into one of the standard search paths. + target_path = Path("~/.config/vulkan/icd.d").expanduser() + print(f"Copying icd file to {target_path}") + target_path.mkdir(parents=True, exist_ok=True) + shutil.copy(icd_json_path, target_path) + + return env_vars + + +def setup_lavapipe_for_windows() -> dict[str, str]: + """Sets up lavapipe mesa driver for Windows (x64).""" + + # Download mesa + run([ + "curl.exe", + "-L", + "--retry", + "5", + f"https://github.com/pal1000/mesa-dist-win/releases/download/{MESA_VERSION}/mesa3d-{MESA_VERSION}-release-msvc.7z", + "-o", + "mesa.7z", + ]) + + # Extract needed files + run([ + "7z.exe", + "e", + "mesa.7z", + "-aoa", + "-omesa", + "x64/vulkan_lvp.dll", + "x64/lvp_icd.x86_64.json", + ]) + + # Copy files to target directory. + copy_tree("mesa", TARGET_DIR) + copy_tree("mesa", TARGET_DIR / "deps") + + # Print icd file that should be used. + icd_json_path = Path(os.path.join(os.getcwd(), "mesa", "lvp_icd.x86_64.json")).resolve() + print(f"Using ICD file at '{icd_json_path}':") + with open(icd_json_path, encoding="utf-8") as f: + print(f.read()) + icd_json_path = icd_json_path.as_posix().replace("/", "\\") + + # Set environment variables, make sure to use windows path style. + vulkan_runtime_path = f"{os.environ['VULKAN_SDK']}/runtime/x64" + env_vars = { + "VK_DRIVER_FILES": icd_json_path, + # Vulkan runtime install should do this, but the CI action we're using right now for instance doesn't, + # causing `vulkaninfo` to fail since it can't find the vulkan loader. + "PATH": f"{os.environ.get('PATH', '')};{vulkan_runtime_path}", + } + set_environment_variables(env_vars) + + # For debugging: List files in Vulkan runtime path. + if False: + print(f"\nListing files in Vulkan runtime path '{vulkan_runtime_path}':") + try: + files = os.listdir(vulkan_runtime_path) + for file in files: + print(f" {file}") + except Exception as e: + print(f"Error listing Vulkan runtime directory: {e}") + + # On CI we run with elevated privileges, therefore VK_DRIVER_FILES is ignored. + # See: https://github.com/KhronosGroup/Vulkan-Loader/blob/sdk-1.3.261/docs/LoaderInterfaceArchitecture.md#elevated-privilege-caveats + # Therefore, we have to set one of the registry keys that is checked to find the driver. + # See: https://vulkan.lunarg.com/doc/view/1.3.243.0/windows/LoaderDriverInterface.html#user-content-driver-discovery-on-windows + + # Write registry keys to configure Vulkan drivers + import winreg + + key_path = "SOFTWARE\\Khronos\\Vulkan\\Drivers" + key = winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, key_path) + winreg.SetValueEx(key, icd_json_path, 0, winreg.REG_DWORD, 0) + winreg.CloseKey(key) + + return env_vars + + +def vulkan_info(extra_env_vars: dict[str, str]) -> None: + vulkan_sdk_path = os.environ["VULKAN_SDK"] + env = os.environ.copy() + env["VK_LOADER_DEBUG"] = "all" # Enable verbose logging of vulkan loader for debugging. + for key, value in extra_env_vars.items(): + env[key] = value + + if os.name == "nt": + vulkaninfo_path = f"{vulkan_sdk_path}/bin/vulkaninfoSDK.exe" + else: + vulkaninfo_path = f"{vulkan_sdk_path}/x86_64/bin/vulkaninfo" + print(run([vulkaninfo_path], env=env).stdout) + + +def check_for_vulkan_sdk() -> None: + vulkan_sdk_path = os.environ.get("VULKAN_SDK") + if vulkan_sdk_path is None: + print( + "ERROR: VULKAN_SDK is not set. The sdk needs to be installed prior including runtime & vulkaninfo utility." + ) + sys.exit(1) + + +def main() -> None: + if os.name == "nt" and platform.machine() == "AMD64": + # Note that we could also use WARP, the DX12 software rasterizer. + # (wgpu tests with both llvmpip and WARP) + # But practically speaking we prefer Vulkan anyways on Windows today and as such this is + # both less variation and closer to what Rerun uses when running on a "real" machine. + check_for_vulkan_sdk() + env_vars = setup_lavapipe_for_windows() + vulkan_info(env_vars) + elif os.name == "posix" and sys.platform != "darwin" and platform.machine() == "x86_64": + check_for_vulkan_sdk() + env_vars = setup_lavapipe_for_linux() + vulkan_info(env_vars) + elif os.name == "posix" and sys.platform == "darwin": + print("Skipping software rasterizer setup for macOS - we have to rely on a real GPU here.") + else: + raise ValueError(f"Unsupported OS / architecture: {os.name} / {platform.machine()}") + + +if __name__ == "__main__": + main()