build.rs

  1#![cfg_attr(any(not(target_os = "macos"), feature = "macos-blade"), allow(unused))]
  2
  3//TODO: consider generating shader code for WGSL
  4//TODO: deprecate "runtime-shaders" and "macos-blade"
  5
  6use std::env;
  7
  8fn main() {
  9    let target = env::var("CARGO_CFG_TARGET_OS");
 10    println!("cargo::rustc-check-cfg=cfg(gles)");
 11
 12    #[cfg(any(
 13        not(any(target_os = "macos", target_os = "windows")),
 14        feature = "macos-blade"
 15    ))]
 16    check_wgsl_shaders();
 17
 18    match target.as_deref() {
 19        Ok("macos") => {
 20            #[cfg(target_os = "macos")]
 21            macos::build();
 22        }
 23        Ok("windows") => {
 24            #[cfg(target_os = "windows")]
 25            windows::build();
 26        }
 27        _ => (),
 28    };
 29}
 30
 31#[allow(dead_code)]
 32fn check_wgsl_shaders() {
 33    use std::path::PathBuf;
 34    use std::process;
 35    use std::str::FromStr;
 36
 37    let shader_source_path = "./src/platform/blade/shaders.wgsl";
 38    let shader_path = PathBuf::from_str(shader_source_path).unwrap();
 39    println!("cargo:rerun-if-changed={}", &shader_path.display());
 40
 41    let shader_source = std::fs::read_to_string(&shader_path).unwrap();
 42
 43    match naga::front::wgsl::parse_str(&shader_source) {
 44        Ok(_) => {
 45            // All clear
 46        }
 47        Err(e) => {
 48            eprintln!("WGSL shader compilation failed:\n{}", e);
 49            process::exit(1);
 50        }
 51    }
 52}
 53#[cfg(target_os = "macos")]
 54mod macos {
 55    use std::{
 56        env,
 57        path::{Path, PathBuf},
 58    };
 59
 60    use cbindgen::Config;
 61
 62    pub(super) fn build() {
 63        generate_dispatch_bindings();
 64        #[cfg(not(feature = "macos-blade"))]
 65        {
 66            let header_path = generate_shader_bindings();
 67
 68            #[cfg(feature = "runtime_shaders")]
 69            emit_stitched_shaders(&header_path);
 70            #[cfg(not(feature = "runtime_shaders"))]
 71            compile_metal_shaders(&header_path);
 72        }
 73    }
 74
 75    fn generate_dispatch_bindings() {
 76        println!("cargo:rustc-link-lib=framework=System");
 77
 78        let bindings = bindgen::Builder::default()
 79            .header("src/platform/mac/dispatch.h")
 80            .allowlist_var("_dispatch_main_q")
 81            .allowlist_var("_dispatch_source_type_data_add")
 82            .allowlist_var("DISPATCH_QUEUE_PRIORITY_HIGH")
 83            .allowlist_var("DISPATCH_TIME_NOW")
 84            .allowlist_function("dispatch_get_global_queue")
 85            .allowlist_function("dispatch_async_f")
 86            .allowlist_function("dispatch_after_f")
 87            .allowlist_function("dispatch_time")
 88            .allowlist_function("dispatch_source_merge_data")
 89            .allowlist_function("dispatch_source_create")
 90            .allowlist_function("dispatch_source_set_event_handler_f")
 91            .allowlist_function("dispatch_resume")
 92            .allowlist_function("dispatch_suspend")
 93            .allowlist_function("dispatch_source_cancel")
 94            .allowlist_function("dispatch_set_context")
 95            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
 96            .layout_tests(false)
 97            .generate()
 98            .expect("unable to generate bindings");
 99
100        let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
101        bindings
102            .write_to_file(out_path.join("dispatch_sys.rs"))
103            .expect("couldn't write dispatch bindings");
104    }
105
106    fn generate_shader_bindings() -> PathBuf {
107        let output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("scene.h");
108        let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
109        let mut config = Config {
110            include_guard: Some("SCENE_H".into()),
111            language: cbindgen::Language::C,
112            no_includes: true,
113            ..Default::default()
114        };
115        config.export.include.extend([
116            "Bounds".into(),
117            "Corners".into(),
118            "Edges".into(),
119            "Size".into(),
120            "Pixels".into(),
121            "PointF".into(),
122            "Hsla".into(),
123            "ContentMask".into(),
124            "Uniforms".into(),
125            "AtlasTile".into(),
126            "PathRasterizationInputIndex".into(),
127            "PathVertex_ScaledPixels".into(),
128            "PathRasterizationVertex".into(),
129            "ShadowInputIndex".into(),
130            "Shadow".into(),
131            "QuadInputIndex".into(),
132            "Underline".into(),
133            "UnderlineInputIndex".into(),
134            "Quad".into(),
135            "BorderStyle".into(),
136            "SpriteInputIndex".into(),
137            "MonochromeSprite".into(),
138            "PolychromeSprite".into(),
139            "PathSprite".into(),
140            "SurfaceInputIndex".into(),
141            "SurfaceBounds".into(),
142            "TransformationMatrix".into(),
143        ]);
144        config.no_includes = true;
145        config.enumeration.prefix_with_name = true;
146
147        let mut builder = cbindgen::Builder::new();
148
149        let src_paths = [
150            crate_dir.join("src/scene.rs"),
151            crate_dir.join("src/geometry.rs"),
152            crate_dir.join("src/color.rs"),
153            crate_dir.join("src/window.rs"),
154            crate_dir.join("src/platform.rs"),
155            crate_dir.join("src/platform/mac/metal_renderer.rs"),
156        ];
157        for src_path in src_paths {
158            println!("cargo:rerun-if-changed={}", src_path.display());
159            builder = builder.with_src(src_path);
160        }
161
162        builder
163            .with_config(config)
164            .generate()
165            .expect("Unable to generate bindings")
166            .write_to_file(&output_path);
167
168        output_path
169    }
170
171    /// To enable runtime compilation, we need to "stitch" the shaders file with the generated header
172    /// so that it is self-contained.
173    #[cfg(feature = "runtime_shaders")]
174    fn emit_stitched_shaders(header_path: &Path) {
175        use std::str::FromStr;
176        fn stitch_header(header: &Path, shader_path: &Path) -> std::io::Result<PathBuf> {
177            let header_contents = std::fs::read_to_string(header)?;
178            let shader_contents = std::fs::read_to_string(shader_path)?;
179            let stitched_contents = format!("{header_contents}\n{shader_contents}");
180            let out_path =
181                PathBuf::from(env::var("OUT_DIR").unwrap()).join("stitched_shaders.metal");
182            std::fs::write(&out_path, stitched_contents)?;
183            Ok(out_path)
184        }
185        let shader_source_path = "./src/platform/mac/shaders.metal";
186        let shader_path = PathBuf::from_str(shader_source_path).unwrap();
187        stitch_header(header_path, &shader_path).unwrap();
188        println!("cargo:rerun-if-changed={}", &shader_source_path);
189    }
190
191    #[cfg(not(feature = "runtime_shaders"))]
192    fn compile_metal_shaders(header_path: &Path) {
193        use std::process::{self, Command};
194        let shader_path = "./src/platform/mac/shaders.metal";
195        let air_output_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.air");
196        let metallib_output_path =
197            PathBuf::from(env::var("OUT_DIR").unwrap()).join("shaders.metallib");
198        println!("cargo:rerun-if-changed={}", shader_path);
199
200        let output = Command::new("xcrun")
201            .args([
202                "-sdk",
203                "macosx",
204                "metal",
205                "-gline-tables-only",
206                "-mmacosx-version-min=10.15.7",
207                "-MO",
208                "-c",
209                shader_path,
210                "-include",
211                (header_path.to_str().unwrap()),
212                "-o",
213            ])
214            .arg(&air_output_path)
215            .output()
216            .unwrap();
217
218        if !output.status.success() {
219            eprintln!(
220                "metal shader compilation failed:\n{}",
221                String::from_utf8_lossy(&output.stderr)
222            );
223            process::exit(1);
224        }
225
226        let output = Command::new("xcrun")
227            .args(["-sdk", "macosx", "metallib"])
228            .arg(air_output_path)
229            .arg("-o")
230            .arg(metallib_output_path)
231            .output()
232            .unwrap();
233
234        if !output.status.success() {
235            eprintln!(
236                "metallib compilation failed:\n{}",
237                String::from_utf8_lossy(&output.stderr)
238            );
239            process::exit(1);
240        }
241    }
242}
243
244#[cfg(target_os = "windows")]
245mod windows {
246    use std::{
247        io::Write,
248        path::{Path, PathBuf},
249        process::{self, Command},
250    };
251
252    pub(super) fn build() {
253        // Link the AMD AGS library
254        link_amd_ags();
255
256        // Compile HLSL shaders
257        #[cfg(not(debug_assertions))]
258        compile_shaders();
259
260        // Embed the Windows manifest and resource file
261        #[cfg(feature = "windows-manifest")]
262        embed_resource();
263    }
264
265    fn link_amd_ags() {
266        // We can not use relative paths in `cargo:rustc-link-search`, so we need to use the absolute path.
267        // See: https://stackoverflow.com/questions/41917096/how-do-i-make-rustc-link-search-relative-to-the-project-location
268        let lib_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("libs");
269        #[cfg(target_pointer_width = "64")]
270        let lib_name = "amd_ags_x64_2022_MT";
271        #[cfg(target_pointer_width = "32")]
272        let lib_name = "amd_ags_x86_2022_MT";
273        println!("cargo:rustc-link-lib=static={}", lib_name);
274        println!("cargo:rustc-link-search=native={}", lib_dir.display());
275        println!(
276            "cargo:rerun-if-changed={}/{}.lib",
277            lib_dir.display(),
278            lib_name
279        );
280    }
281
282    #[cfg(feature = "windows-manifest")]
283    fn embed_resource() {
284        let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
285        let rc_file = std::path::Path::new("resources/windows/gpui.rc");
286        println!("cargo:rerun-if-changed={}", manifest.display());
287        println!("cargo:rerun-if-changed={}", rc_file.display());
288        embed_resource::compile(rc_file, embed_resource::NONE)
289            .manifest_required()
290            .unwrap();
291    }
292
293    /// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
294    fn compile_shaders() {
295        use std::fs;
296
297        let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
298            .join("src/platform/windows/shaders.hlsl");
299        let out_dir = std::env::var("OUT_DIR").unwrap();
300
301        println!("cargo:rerun-if-changed={}", shader_path.display());
302
303        // Check if fxc.exe is available
304        let fxc_path = find_fxc_compiler();
305
306        // Define all modules
307        let modules = [
308            "quad",
309            "shadow",
310            "paths",
311            "underline",
312            "monochrome_sprite",
313            "polychrome_sprite",
314        ];
315
316        let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
317        if Path::new(&rust_binding_path).exists() {
318            fs::remove_file(&rust_binding_path)
319                .expect("Failed to remove existing Rust binding file");
320        }
321        for module in &modules {
322            compile_shader_for_module(
323                module,
324                &out_dir,
325                &fxc_path,
326                shader_path.to_str().unwrap(),
327                &rust_binding_path,
328            );
329        }
330        println!(
331            "cargo:warning=Successfully compiled shaders. Output written to: {}",
332            rust_binding_path
333        );
334    }
335
336    fn find_fxc_compiler() -> String {
337        // Try to find in PATH
338        if let Ok(output) = std::process::Command::new("where").arg("fxc.exe").output() {
339            if output.status.success() {
340                let path = String::from_utf8_lossy(&output.stdout);
341                return path.trim().to_string();
342            }
343        }
344
345        // Check the default path
346        if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
347            .exists()
348        {
349            return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
350                .to_string();
351        }
352
353        // Check environment variable
354        if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
355            if Path::new(&path).exists() {
356                return path;
357            }
358        }
359
360        panic!("Failed to find fxc.exe");
361    }
362
363    fn compile_shader_for_module(
364        module: &str,
365        out_dir: &str,
366        fxc_path: &str,
367        shader_path: &str,
368        rust_binding_path: &str,
369    ) {
370        // Compile vertex shader
371        let output_file = format!("{}/{}_vs.h", out_dir, module);
372        let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
373        compile_shader_impl(
374            fxc_path,
375            &format!("{module}_vertex"),
376            &output_file,
377            &const_name,
378            shader_path,
379            "vs_5_0",
380        );
381        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
382
383        // Compile fragment shader
384        let output_file = format!("{}/{}_ps.h", out_dir, module);
385        let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
386        compile_shader_impl(
387            fxc_path,
388            &format!("{module}_fragment"),
389            &output_file,
390            &const_name,
391            shader_path,
392            "ps_5_0",
393        );
394        generate_rust_binding(&const_name, &output_file, &rust_binding_path);
395    }
396
397    fn compile_shader_impl(
398        fxc_path: &str,
399        entry_point: &str,
400        output_path: &str,
401        var_name: &str,
402        shader_path: &str,
403        target: &str,
404    ) {
405        let output = Command::new(fxc_path)
406            .args([
407                "/T",
408                target,
409                "/E",
410                entry_point,
411                "/Fh",
412                output_path,
413                "/Vn",
414                var_name,
415                "/O3",
416                shader_path,
417            ])
418            .output();
419
420        match output {
421            Ok(result) => {
422                if result.status.success() {
423                    return;
424                }
425                eprintln!(
426                    "Pixel shader compilation failed for {}:\n{}",
427                    entry_point,
428                    String::from_utf8_lossy(&result.stderr)
429                );
430                process::exit(1);
431            }
432            Err(e) => {
433                eprintln!("Failed to run fxc for {}: {}", entry_point, e);
434                process::exit(1);
435            }
436        }
437    }
438
439    fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
440        let header_content =
441            std::fs::read_to_string(head_file).expect("Failed to read header file");
442        let const_definition = {
443            let global_var_start = header_content.find("const BYTE").unwrap();
444            let global_var = &header_content[global_var_start..];
445            let equal = global_var.find('=').unwrap();
446            global_var[equal + 1..].trim()
447        };
448        let rust_binding = format!(
449            "const {}: &[u8] = &{}\n",
450            const_name,
451            const_definition.replace('{', "[").replace('}', "]")
452        );
453        let mut options = std::fs::OpenOptions::new()
454            .write(true)
455            .create(true)
456            .append(true)
457            .open(output_path)
458            .expect("Failed to open Rust binding file");
459        options
460            .write_all(rust_binding.as_bytes())
461            .expect("Failed to write Rust binding file");
462    }
463}