kitgit

tirbofish/dropbear · diff

98da270 · Thribhu K

Merge pull request #86 from tirbofish/slang-shaders feature: slang shaders jarvis, run github actions

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index 9042336..8d56500 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -75,6 +75,8 @@ postcard = { version = "1.1"}
 pollster = "0.4"
 yakui = { git = "https://github.com/SecondHalfGames/yakui", rev = "ddf7614" }
 yakui-wgpu = { git = "https://github.com/SecondHalfGames/yakui", rev = "ddf7614" }
+thiserror = "2.0"
+tempfile = "3.24"
 
 [workspace.dependencies.image]
 version = "0.25"
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index 8897280..5a68677 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -12,6 +12,7 @@ readme.workspace = true
 dropbear_future-queue = { path = "../dropbear_future-queue" }
 dropbear-traits = { path = "../dropbear-traits" }
 dropbear-macro = { path = "../dropbear-macro" }
+slank = { path = "../slank", features = ["download-slang", "use-wgpu"] }
 
 anyhow.workspace = true
 app_dirs2.workspace = true
@@ -53,3 +54,6 @@ rfd.workspace = true
 version = "0.25"
 default-features = false
 features = ["png", "jpeg"]
+
+[build-dependencies]
+slank = { path = "../slank", features = ["download-slang"] }
\ No newline at end of file
diff --git a/crates/dropbear-engine/build.rs b/crates/dropbear-engine/build.rs
new file mode 100644
index 0000000..7f01aa4
--- /dev/null
+++ b/crates/dropbear-engine/build.rs
@@ -0,0 +1,19 @@
+use slank::{SlangShaderBuilder, SlangTarget};
+
+fn main() {
+    // to copy paste:         
+    // let shader = Shader::from_slang(graphics.clone(), &slank::compiled::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube")));
+
+    SlangShaderBuilder::new("light_cube")
+        .add_source_path("src/pipelines/shaders/light.slang").unwrap()
+        .compile_to_out_dir(SlangTarget::SpirV).unwrap();
+
+    SlangShaderBuilder::new("blit_shader")
+        .add_source_path("src/pipelines/shaders/blit.slang").unwrap()
+        .compile_to_out_dir(SlangTarget::SpirV).unwrap();
+
+    // unable to do mipmap.slang because it required texture_storage_2d, one write and one read. 
+    // just wasn't possible with slang :(
+
+    
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/mipmap.rs b/crates/dropbear-engine/src/mipmap.rs
index d397eca..fe20ae4 100644
--- a/crates/dropbear-engine/src/mipmap.rs
+++ b/crates/dropbear-engine/src/mipmap.rs
@@ -1,4 +1,6 @@
-use crate::texture::Texture;
+use slank::{include_slang, utils::WgpuUtils};
+
+use crate::{texture::Texture};
 
 pub struct MipMapper {
     blit_mipmap: wgpu::RenderPipeline,
@@ -10,10 +12,10 @@ pub struct MipMapper {
 
 impl MipMapper {
     pub fn new(device: &wgpu::Device) -> Self {
-        let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
-            label: Some("mipmap blit shader"),
-            source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/blit.wgsl").into()),
-        });
+        let blit_shader = device.create_shader_module(slank::compiled::CompiledSlangShader::from_bytes(
+            "mipmap blit_shader", 
+            include_slang!("blit_shader")
+        ).create_wgpu_shader());
 
         // Keep this SRGB so we can render directly into the SRGB textures we create for materials.
         let blit_format = Texture::TEXTURE_FORMAT;
@@ -88,10 +90,12 @@ impl MipMapper {
             bind_group_layouts: &[&storage_texture_layout],
             push_constant_ranges: &[],
         });
+
         let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
             label: Some("mipmap compute shader"),
             source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/mipmap.wgsl").into()),
         });
+
         let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
             label: Some("Mipmapper"),
             layout: Some(&pipeline_layout),
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index 6a599a9..3975d61 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -1,6 +1,7 @@
 use std::sync::Arc;
 use std::mem::size_of;
 use glam::DMat4;
+use slank::include_slang;
 use wgpu::{BufferAddress, VertexAttribute, VertexFormat};
 use crate::buffer::{StorageBuffer, UniformBuffer};
 use crate::entity::{EntityTransform, Transform};
@@ -26,11 +27,7 @@ pub struct LightCubePipeline {
 
 impl DropbearShaderPipeline for LightCubePipeline {
     fn new(graphics: Arc<SharedGraphicsContext>) -> Self {
-        let shader = Shader::new(
-            graphics.clone(),
-            include_str!("shaders/light.wgsl"),
-            Some("light cube shader"),
-        );
+        let shader = Shader::from_slang(graphics.clone(), &slank::compiled::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube")));
 
         let pipeline_layout = graphics.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
             label: Some("light cube pipeline layout"),
diff --git a/crates/dropbear-engine/src/pipelines/shaders/blit.slang b/crates/dropbear-engine/src/pipelines/shaders/blit.slang
new file mode 100644
index 0000000..4b14a95
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/shaders/blit.slang
@@ -0,0 +1,35 @@
+#include "dropbear.slang"
+
+struct VertexOutput {
+    float4 clip_position : SV_Position;
+
+    [[vk_location(0)]]
+    float2 uv;
+};
+
+[[vk_binding(0, 0)]]
+Texture2D tex;
+
+[[vk_binding(1, 0)]]
+SamplerState tex_sampler;
+
+[shader("vertex")]
+VertexOutput vs_main(
+    uint in_vertex_index: SV_VulkanVertexID
+) {
+    VertexOutput output;
+
+    float x = float((in_vertex_index << 1u) & 2u);
+    float y = float(in_vertex_index & 2u);
+
+    output.clip_position = float4(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
+    output.uv = float2(x, 1.0 - y);
+
+    return output;
+}
+
+[shader("fragment")]
+float4 fs_main(VertexOutput input) : SV_Target0 {
+    float4 s = tex.Sample(tex_sampler, input.uv);
+    return s;
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl b/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl
deleted file mode 100644
index aa10b01..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl
+++ /dev/null
@@ -1,28 +0,0 @@
-struct VertexOutput {
-    @builtin(position) clip_position: vec4<f32>,
-    @location(0) uv: vec2<f32>,
-}
-
-@group(0) @binding(0)
-var tex: texture_2d<f32>;
-@group(0) @binding(1)
-var tex_sampler: sampler;
-
-@vertex
-fn vs_main(
-    @builtin(vertex_index) in_vertex_index: u32,
-) -> VertexOutput {
-    var out: VertexOutput;
-    // Create fullscreen triangle
-    let x = f32((in_vertex_index << 1u) & 2u);
-    let y = f32(in_vertex_index & 2u);
-    out.clip_position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
-    out.uv = vec2<f32>(x, 1.0 - y);
-    return out;
-}
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    let s = textureSample(tex, tex_sampler, in.uv);
-    return s;
-}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/dropbear.slang b/crates/dropbear-engine/src/pipelines/shaders/dropbear.slang
new file mode 100644
index 0000000..ff6f1d2
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/shaders/dropbear.slang
@@ -0,0 +1,45 @@
+struct Globals {
+    uint num_lights;
+    float ambient_strength;
+};
+
+/// A standard light descriptor struct. 
+struct Light {
+    float4 position;
+    float4 direction; // x, y, z, outer_cutoff_angle
+    float4 color; // r, g, b, light_type (0, 1, 2)
+
+    float constant;
+    float lin;
+    float quadratic;
+
+    float cutoff;
+};
+
+struct CameraUniform {
+    float4 view_pos;
+    float4x4 view_proj;
+};
+
+struct MaterialUniform {
+    // for stuff like tinting
+    float4 colour;
+
+    // scales incoming UVs before sampling
+    float2 uv_tiling;
+};
+
+/// Converts a wgpu matrix (in the form of 4 float4) into a matrix usable for Slang. 
+float4x4 vs_input_matrix(
+    float4 mat1, 
+    float4 mat2, 
+    float4 mat3, 
+    float4 mat4
+) {
+    return transpose(float4x4(
+        mat1,
+        mat2,
+        mat3,
+        mat4
+    ));
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/light.slang b/crates/dropbear-engine/src/pipelines/shaders/light.slang
new file mode 100644
index 0000000..9a2a330
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/shaders/light.slang
@@ -0,0 +1,51 @@
+#include "dropbear.slang"
+
+[[vk::binding(0, 0)]]
+ConstantBuffer<CameraUniform> camera;
+
+[[vk::binding(0, 1)]]
+ConstantBuffer<Light> light;
+
+struct VertexInput {
+    [[vk::location(0)]]
+    float3 position : POSITION;
+    
+    [[vk::location(5)]]
+    float4 model_matrix_0 : TEXCOORD5;
+    
+    [[vk::location(6)]]
+    float4 model_matrix_1 : TEXCOORD6;
+    
+    [[vk::location(7)]]
+    float4 model_matrix_2 : TEXCOORD7;
+    
+    [[vk::location(8)]]
+    float4 model_matrix_3 : TEXCOORD8;
+};
+
+struct VertexOutput {
+    float4 clip_position : SV_Position;
+    
+    [[vk::location(0)]]
+    float3 color : COLOR0;
+};
+
+[shader("vertex")]
+VertexOutput vs_main(VertexInput input) {
+    float4x4 model_matrix = vs_input_matrix(
+        input.model_matrix_0,
+        input.model_matrix_1,
+        input.model_matrix_2,
+        input.model_matrix_3,
+    );
+
+    VertexOutput output;
+    output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0)));
+    output.color = light.color.xyz;
+    return output;
+}
+
+[shader("fragment")]
+float4 fs_main(VertexOutput input) : SV_Target0 {
+    return float4(input.color, 1.0);
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/light.wgsl b/crates/dropbear-engine/src/pipelines/shaders/light.wgsl
deleted file mode 100644
index d31ebc8..0000000
--- a/crates/dropbear-engine/src/pipelines/shaders/light.wgsl
+++ /dev/null
@@ -1,62 +0,0 @@
-// Shader for rendering the white cube for lighting (or diff depending on colour)
-
-struct Light {
-    position: vec4<f32>,
-    direction: vec4<f32>, // x, y, z, outer_cutoff_angle
-    color: vec4<f32>, // r, g, b, light_type (0, 1, 2)
-    constant: f32,
-    lin: f32,
-    quadratic: f32,
-    cutoff: f32,
-}
-
-struct CameraUniform {
-    view_pos: vec4<f32>,
-    view_proj: mat4x4<f32>,
-};
-
-@group(0) @binding(0)
-var<uniform> camera: CameraUniform;
-
-@group(1) @binding(0)
-var<uniform> light: Light;
-
-struct InstanceInput {
-    @location(5) model_matrix_0: vec4<f32>,
-    @location(6) model_matrix_1: vec4<f32>,
-    @location(7) model_matrix_2: vec4<f32>,
-    @location(8) model_matrix_3: vec4<f32>,
-}
-
-struct VertexInput {
-    @location(0) position: vec3<f32>,
-};
-
-struct VertexOutput {
-    @builtin(position) clip_position: vec4<f32>,
-    @location(0) color: vec3<f32>,
-};
-
-@vertex
-fn vs_main(
-    model: VertexInput,
-    instance: InstanceInput,
-) -> VertexOutput {
-    let model_matrix = mat4x4<f32>(
-        instance.model_matrix_0,
-        instance.model_matrix_1,
-        instance.model_matrix_2,
-        instance.model_matrix_3,
-    );
-    var out: VertexOutput;
-    out.clip_position = camera.view_proj * model_matrix * vec4<f32>(model.position, 1.0);
-    out.color = light.color.xyz;
-    return out;
-}
-
-// Fragment shaders
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
-    return vec4<f32>(in.color, 1.0);
-}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/shader.rs b/crates/dropbear-engine/src/shader.rs
index 772572f..4681432 100644
--- a/crates/dropbear-engine/src/shader.rs
+++ b/crates/dropbear-engine/src/shader.rs
@@ -3,6 +3,7 @@
 use std::ops::Deref;
 use crate::graphics::SharedGraphicsContext;
 use std::sync::Arc;
+use slank::{compiled::CompiledSlangShader, utils::WgpuUtils};
 use wgpu::ShaderModule;
 
 /// A nice little struct that stored basic information about a WGPU shaders.
@@ -47,6 +48,8 @@ impl Shader {
 
         log::debug!("Created new shaders under the label: {:?}", label);
 
+        slank::compiled::CompiledSlangShader::from_bytes("light cube", slank::include_slang!("light_cube"));
+
         Self {
             label: match label {
                 Some(label) => label.into(),
@@ -56,4 +59,18 @@ impl Shader {
             content: shader_file_contents.to_string(),
         }
     }
+
+    pub fn from_slang(graphics: Arc<SharedGraphicsContext>, shader: &CompiledSlangShader) -> Self {
+        let module = graphics
+            .device
+            .create_shader_module(shader.create_wgpu_shader());
+
+        log::debug!("Created new shaders [slang] under the label: {}", shader.label());
+
+        Self {
+            label: shader.label().clone(),
+            module,
+            content: String::from_utf8(shader.source.clone()).unwrap_or_default(),
+        }
+    }
 }
diff --git a/crates/slank/Cargo.toml b/crates/slank/Cargo.toml
new file mode 100644
index 0000000..09ea073
--- /dev/null
+++ b/crates/slank/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "slank"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+readme = "README.md"
+
+[dependencies]
+
+wgpu = { workspace = true, optional = true }
+thiserror.workspace = true
+tempfile.workspace = true
+anyhow.workspace = true
+
+[features]
+# this feature downloads the slang compiler for the correct operating system and stores it for later.
+download-slang = []
+
+# allows for the wgpu dependency to be available.
+use-wgpu = ["wgpu/default", "wgpu/spirv"]
+
+[build-dependencies]
+dirs = "6.0.0"
+reqwest = { version = "0.13.1", features = ["blocking", "json"] }
+zip = "7.2.0"
+flate2 = "1.1.8"
+tar = "0.4.44"
+anyhow.workspace = true
+serde = { workspace = true }
+serde_json = "1.0"
\ No newline at end of file
diff --git a/crates/slank/README.md b/crates/slank/README.md
new file mode 100644
index 0000000..5cfce7e
--- /dev/null
+++ b/crates/slank/README.md
@@ -0,0 +1,4 @@
+# slank
+
+A rust version of the slang compiler, ready to be used by wgpu, or vulkan (or any graphics library really, i personally
+use wgpu). 
\ No newline at end of file
diff --git a/crates/slank/build.rs b/crates/slank/build.rs
new file mode 100644
index 0000000..0def8ad
--- /dev/null
+++ b/crates/slank/build.rs
@@ -0,0 +1,283 @@
+use std::path::PathBuf;
+use std::process::Command;
+
+fn main() -> anyhow::Result<()> {
+    println!("cargo:rerun-if-changed=build.rs");
+
+    let path = find_or_download_slangc()?;
+
+    println!("cargo:rustc-env=SLANG_DIR={}", path.display());
+    // println!("cargo:warning=Found slangc at: {}", path.display());
+    Ok(())
+}
+
+fn find_or_download_slangc() -> anyhow::Result<PathBuf> {
+    if let Ok(path) = std::env::var("SLANG_DIR") {
+        let path = PathBuf::from(path);
+        let exe_path = path.join("bin").join("slangc");
+        if is_valid_slangc(&exe_path)? {
+            return Ok(path);
+        } else {
+            anyhow::bail!("Not valid slangc installation at: {}", path.display());
+        }
+    }
+
+    if let Some(exe_path) = find_in_path("slangc") {
+        if is_valid_slangc(&exe_path)? {
+            if let Some(root) = exe_path.parent().and_then(|p| p.parent()) {
+                return Ok(root.to_path_buf());
+            }
+        }
+    }
+
+    if let Some(exe_path) = check_cached_download() {
+        if is_valid_slangc(&exe_path)? {
+            if let Some(root) = exe_path.parent().and_then(|p| p.parent()) {
+                return Ok(root.to_path_buf());
+            }
+        }
+    }
+
+    #[cfg(feature = "download-slang")]
+    {
+        return download_slang();
+    }
+
+    #[cfg(not(feature = "download-slang"))]
+    {
+        Err(anyhow::anyhow!(
+            "slangc not found. Either:\n\
+             1. Install slangc and add it to PATH\n\
+             2. Set SLANG_DIR environment variable\n\
+             3. Enable the 'download-slang' feature"
+        ))
+    }
+}
+
+fn is_valid_slangc(path: &PathBuf) -> anyhow::Result<bool> {
+    if !path.exists() {
+        return Ok(false);
+    }
+
+    Ok(Command::new(path)
+        .arg("-version")
+        .output()
+        .map(|output| output.status.success())
+        .unwrap_or(false))
+}
+
+fn find_in_path(name: &str) -> Option<PathBuf> {
+    std::env::var_os("PATH").and_then(|paths| {
+        std::env::split_paths(&paths)
+            .filter_map(|dir| {
+                let full_path = dir.join(name);
+                // On Windows, also try with .exe extension
+                #[cfg(target_os = "windows")]
+                {
+                    if full_path.with_extension("exe").exists() {
+                        return Some(full_path.with_extension("exe"));
+                    }
+                }
+                if full_path.exists() {
+                    Some(full_path)
+                } else {
+                    None
+                }
+            })
+            .next()
+    })
+}
+
+fn check_cached_download() -> Option<PathBuf> {
+    if let Ok(out_dir) = std::env::var("OUT_DIR") {
+        let cached = PathBuf::from(out_dir)
+            .join("slangc")
+            .join("bin")
+            .join(if cfg!(target_os = "windows") {
+                "slangc.exe"
+            } else {
+                "slangc"
+            });
+
+        if cached.exists() {
+            return Some(cached);
+        }
+    }
+
+    if let Some(cache_dir) = dirs::cache_dir() {
+        let cached = cache_dir
+            .join("slank")
+            .join("slangc")
+            .join("bin")
+            .join(if cfg!(target_os = "windows") {
+                "slangc.exe"
+            } else {
+                "slangc"
+            });
+
+        if cached.exists() {
+            return Some(cached);
+        }
+    }
+
+    None
+}
+
+#[cfg(feature = "download-slang")]
+fn download_slang() -> anyhow::Result<PathBuf> {
+    use std::fs;
+
+    // println!("cargo:warning=Downloading slangc compiler...");
+
+    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
+    let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap();
+
+    let version = get_latest_slang_version()?;
+
+    let (download_url, archive_name) = get_download_url(&version, &target_os, &target_arch)?;
+
+    let cache_dir = dirs::cache_dir()
+        .ok_or_else(|| anyhow::anyhow!("Could not find cache directory"))?
+        .join("slank")
+        .join("slangc");
+
+    fs::create_dir_all(&cache_dir)
+        .map_err(|e| anyhow::anyhow!("Failed to create cache directory: {}", e))?;
+
+    let archive_path = cache_dir.join(&archive_name);
+
+    if !archive_path.exists() {
+        // println!("cargo:warning=Downloading from: {}", download_url);
+        download_file(&download_url, &archive_path)?;
+    } else {
+        // println!("cargo:warning=Using cached download: {}", archive_path.display());
+    }
+
+    extract_archive(&archive_path, &cache_dir)?;
+
+    let slangc_exe = cache_dir
+        .join("bin")
+        .join(if cfg!(target_os = "windows") {
+            "slangc.exe"
+        } else {
+            "slangc"
+        });
+
+    if !slangc_exe.exists() {
+        return Err(anyhow::anyhow!("slangc not found after extraction at: {}", slangc_exe.display()));
+    }
+
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::PermissionsExt;
+        let mut perms = fs::metadata(&slangc_exe)
+            .map_err(|e| anyhow::anyhow!("Failed to get permissions: {}", e))?
+            .permissions();
+        perms.set_mode(0o755);
+        fs::set_permissions(&slangc_exe, perms)
+            .map_err(|e| anyhow::anyhow!("Failed to set permissions: {}", e))?;
+    }
+
+    Ok(cache_dir)
+}
+
+#[cfg(feature = "download-slang")]
+fn get_latest_slang_version() -> anyhow::Result<String> {
+    use serde::Deserialize;
+
+    #[derive(Deserialize)]
+    struct Release {
+        tag_name: String,
+    }
+
+    let client = reqwest::blocking::Client::new();
+    let response = client
+        .get("https://api.github.com/repos/shader-slang/slang/releases/latest")
+        .header(reqwest::header::USER_AGENT, "dropbear-slank-build")
+        .send()
+        .map_err(|e| anyhow::anyhow!("Failed to fetch latest version from GitHub: {}", e))?;
+
+    if !response.status().is_success() {
+        return Err(anyhow::anyhow!("GitHub API failed with status: {}", response.status()));
+    }
+
+    let release: Release = response.json()
+        .map_err(|e| anyhow::anyhow!("Failed to parse GitHub response: {}", e))?;
+
+    Ok(release.tag_name.trim_start_matches('v').to_string())
+}
+
+#[cfg(feature = "download-slang")]
+fn get_download_url(version: &str, os: &str, arch: &str) -> anyhow::Result<(String, String)> {
+    let (platform, ext) = match (os, arch) {
+        ("windows", "x86_64") => ("windows-x86_64", "zip"),
+        ("windows", "aarch64") => ("windows-aarch64", "zip"),
+        ("linux", "x86_64") => ("linux-x86_64", "tar.gz"),
+        ("linux", "aarch64") => ("linux-aarch64", "tar.gz"),
+        ("macos", "x86_64") => ("macos-x86_64", "zip"),
+        ("macos", "aarch64") => ("macos-aarch64", "zip"),
+        _ => return Err(anyhow::anyhow!("Unsupported platform: {}-{}", os, arch)),
+    };
+
+    let archive_name = format!("slang-{}-{}.{}", version, platform, ext);
+    let url = format!(
+        "https://github.com/shader-slang/slang/releases/download/v{}/{}",
+        version, archive_name
+    );
+
+    Ok((url, archive_name))
+}
+
+#[cfg(feature = "download-slang")]
+fn download_file(url: &str, dest: &PathBuf) -> anyhow::Result<()> {
+    use std::fs::File;
+
+    let client = reqwest::blocking::Client::builder()
+        .timeout(std::time::Duration::from_secs(600))
+        .build()
+        .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?;
+
+    let mut response = client.get(url)
+        .header(reqwest::header::USER_AGENT, "dropbear-slank-build")
+        .send()
+        .map_err(|e| anyhow::anyhow!("Failed to download: {}", e))?;
+
+    if !response.status().is_success() {
+        return Err(anyhow::anyhow!("Download failed with status: {}", response.status()));
+    }
+
+    let mut file = File::create(dest)
+        .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?;
+
+    response.copy_to(&mut file)
+        .map_err(|e| anyhow::anyhow!("Failed to save file content at {}: {}", dest.display(), e))?;
+
+    Ok(())
+}
+
+#[cfg(feature = "download-slang")]
+fn extract_archive(archive: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> {
+    use std::fs::File;
+
+    let file = File::open(archive)
+        .map_err(|e| anyhow::anyhow!("Failed to open archive: {}", e))?;
+
+    if archive.extension().and_then(|s| s.to_str()) == Some("zip") {
+        let mut archive = zip::ZipArchive::new(file)
+            .map_err(|e| anyhow::anyhow!("Failed to read zip: {}", e))?;
+
+        archive.extract(dest)
+            .map_err(|e| anyhow::anyhow!("Failed to extract zip: {}", e))?;
+    } else {
+        use flate2::read::GzDecoder;
+        use tar::Archive;
+
+        let tar = GzDecoder::new(file);
+        let mut archive = Archive::new(tar);
+
+        archive.unpack(dest)
+            .map_err(|e| anyhow::anyhow!("Failed to extract tar.gz: {}", e))?;
+    }
+
+    Ok(())
+}
\ No newline at end of file
diff --git a/crates/slank/src/compiled.rs b/crates/slank/src/compiled.rs
new file mode 100644
index 0000000..7237852
--- /dev/null
+++ b/crates/slank/src/compiled.rs
@@ -0,0 +1,47 @@
+use std::path::Path;
+
+pub struct CompiledSlangShader {
+    pub(crate) label: String,
+    args: String,
+    pub source: Vec<u8>,
+}
+
+impl CompiledSlangShader {
+    /// Internal: Creates a new instance of [CompiledSlangShader] from the compiled args
+    /// of [crate::SlangShaderBuilder]
+    pub(crate) fn new(label: String, args: String, source: Vec<u8>) -> Self {
+        Self {
+            args,
+            source,
+            label,
+        }
+    }
+
+    /// Fetches the command line arguments provided into [crate::SlangShaderBuilder]
+    pub fn args(&self) -> String {
+        self.args.clone()
+    }
+    
+    /// Creates a [CompiledSlangShader] from raw bytes.
+    ///
+    /// This is useful when loading shaders compiled at build time using the 
+    /// `include_slang!` macro.
+    pub fn from_bytes(label: &str, source: &[u8]) -> Self {
+        Self {
+            label: label.to_string(),
+            args: String::new(),
+            source: source.to_vec(),
+        }
+    }
+
+    pub fn label(&self) -> String {
+        self.label.clone()
+    }
+
+    /// Writes the output to a file.
+    pub fn output(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
+        let path = path.as_ref();
+
+        std::fs::write(path, &self.source)
+    }
+}
\ No newline at end of file
diff --git a/crates/slank/src/lib.rs b/crates/slank/src/lib.rs
new file mode 100644
index 0000000..681bd38
--- /dev/null
+++ b/crates/slank/src/lib.rs
@@ -0,0 +1,577 @@
+//! slank - slangc for rust. 
+//! 
+//! Compiles slang code during build and stores the shaders locally (or in the crate with [`include_slang`])
+//!
+//! Check out [`SlangShaderBuilder`] to get started.
+
+/// Fetches the slang file (located in {OUT_DIR}/{label}.spv) (assuming it is compiled as .spv) 
+/// and includes the bytes of the file. 
+#[macro_export]
+macro_rules! include_slang {
+    ($label:expr) => {
+        include_bytes!(concat!(env!("OUT_DIR"), "/", $label, ".spv"))
+    };
+}
+
+/// Fetches the path of the shader (with the same label) and returns it to you. 
+#[macro_export]
+macro_rules! include_slang_path {
+    ($label:expr) => {
+        concat!(env!("OUT_DIR"), "/", $label, ".spv")
+    };
+}
+
+pub mod compiled;
+pub mod utils;
+
+use std::{fmt::Display, path::{Path, PathBuf}};
+use crate::compiled::CompiledSlangShader;
+
+#[derive(Debug, Clone)]
+pub struct SourceFile {
+    pub path: Option<PathBuf>,
+    pub content: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct EntryPoint {
+    pub name: String,
+    pub stage: Option<ShaderStage>,
+    /// Index into the sources vec - which file this entry point comes from
+    pub source_index: Option<usize>,
+}
+
+/// Allows you to pass arguments and make an argument builder to chuck into the slangc
+/// compiler.
+///
+/// This is the entry point of the library.
+/// # Usage
+/// 
+/// Add `slank` to your `[build-dependencies]`.
+///
+/// In your `build.rs`:
+/// ```rust,no_run
+/// use slank::{ShaderStage, SlangShaderBuilder, SlangTarget};
+/// use std::path::Path;
+///
+/// fn main() {
+///     let out_dir = std::env::var("OUT_DIR").unwrap();
+///     let dest_path = Path::new(&out_dir).join("shader.spv");
+///
+///     SlangShaderBuilder::new("shader_label")
+///         .add_source_path("src/shader.slang").unwrap()
+///         .entry_with_stage("vs_main", ShaderStage::Vertex)
+///         .entry_with_stage("fs_main", ShaderStage::Fragment)
+///         .build(SlangTarget::SpirV).unwrap()
+///         .output(&dest_path).unwrap();
+/// 
+///     println!("cargo:rerun-if-changed=src/shader.slang");
+/// }
+/// ```
+///
+/// Then in your main code:
+/// ```rust,ignore
+/// let shader_bytes = include_bytes!(concat!(env!("OUT_DIR"), "/shader.spv"));
+/// ```
+pub struct SlangShaderBuilder {
+    label: String,
+    sources: Vec<SourceFile>,
+    entries: Vec<EntryPoint>,
+    profile: Option<Profile>,
+    additional_args: Vec<String>,
+}
+
+impl SlangShaderBuilder {
+    /// Creates a new instance of a [SlangShaderBuilder].
+    pub fn new(label: &str) -> Self {
+        Self {
+            sources: Vec::new(),
+            entries: Vec::new(),
+            label: label.to_string(),
+            profile: None,
+            additional_args: Vec::new(),
+        }
+    }
+
+    /// Adds a source file by path.
+    ///
+    /// Returns the index of this source, which can be used with
+    /// `entry_from_source()` to associate entry points with specific files.
+    pub fn add_source_path(mut self, path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
+        let path_buf = path.as_ref().to_path_buf();
+        let content = std::fs::read_to_string(&path_buf)?;
+
+        self.sources.push(SourceFile {
+            path: Some(path_buf),
+            content,
+        });
+
+        Ok(self)
+    }
+
+    /// Adds a source file by path (non-consuming version).
+    pub fn source_path(&mut self, path: impl AsRef<Path>) -> Result<usize, std::io::Error> {
+        let path_buf = path.as_ref().to_path_buf();
+        let content = std::fs::read_to_string(&path_buf)?;
+
+        self.sources.push(SourceFile {
+            path: Some(path_buf),
+            content,
+        });
+
+        Ok(self.sources.len() - 1)
+    }
+
+    /// Adds source code as a string.
+    ///
+    /// Returns the index of this source.
+    pub fn add_source_str(mut self, content: &str) -> Self {
+        self.sources.push(SourceFile {
+            path: None,
+            content: content.to_string(),
+        });
+        self
+    }
+
+    /// Adds source code as a string (non-consuming version).
+    pub fn source_str(&mut self, content: &str) -> usize {
+        self.sources.push(SourceFile {
+            path: None,
+            content: content.to_string(),
+        });
+        self.sources.len() - 1
+    }
+
+    /// Adds multiple source files at once.
+    pub fn add_source_paths(mut self, paths: &[impl AsRef<Path>]) -> Result<Self, std::io::Error> {
+        for path in paths {
+            let path_buf = path.as_ref().to_path_buf();
+            let content = std::fs::read_to_string(&path_buf)?;
+
+            self.sources.push(SourceFile {
+                path: Some(path_buf),
+                content,
+            });
+        }
+        Ok(self)
+    }
+
+    /// Adds an entry point from the most recently added source.
+    ///
+    /// According to Slang docs: "the file associated with the entry point
+    /// will be the first one found when searching to the left in the command line."
+    pub fn entry(mut self, name: &str) -> Self {
+        let source_index = if self.sources.is_empty() {
+            None
+        } else {
+            Some(self.sources.len() - 1)
+        };
+
+        self.entries.push(EntryPoint {
+            name: name.to_string(),
+            stage: None,
+            source_index,
+        });
+        self
+    }
+
+    /// Adds an entry point with an explicit shader stage.
+    pub fn entry_with_stage(mut self, name: &str, stage: ShaderStage) -> Self {
+        let source_index = if self.sources.is_empty() {
+            None
+        } else {
+            Some(self.sources.len() - 1)
+        };
+
+        self.entries.push(EntryPoint {
+            name: name.to_string(),
+            stage: Some(stage),
+            source_index,
+        });
+        self
+    }
+
+    /// Adds an entry point from a specific source file (by index).
+    pub fn entry_from_source(mut self, name: &str, source_index: usize) -> Self {
+        self.entries.push(EntryPoint {
+            name: name.to_string(),
+            stage: None,
+            source_index: Some(source_index),
+        });
+        self
+    }
+
+    /// Adds an entry point with stage from a specific source file.
+    pub fn entry_from_source_with_stage(
+        mut self,
+        name: &str,
+        source_index: usize,
+        stage: ShaderStage
+    ) -> Self {
+        self.entries.push(EntryPoint {
+            name: name.to_string(),
+            stage: Some(stage),
+            source_index: Some(source_index),
+        });
+        self
+    }
+
+    pub fn with_profile(mut self, profile: Profile) -> Self {
+        self.profile = Some(profile);
+        self
+    }
+
+    /// In the case that there was an argument not available to this builder, you can
+    /// manually provide it here. 
+    pub fn with_additional_args(mut self, args: &[&str]) -> Self {
+        self.additional_args = args.to_vec().iter().map(|v| v.to_string()).collect::<Vec<_>>();
+        self
+    }
+
+    pub fn compile_to_out_dir(self, target: SlangTarget) -> anyhow::Result<()> {
+        let label = self.label.clone();
+        let compiled = self.build(target)?;
+        let out_dir = std::env::var("OUT_DIR")
+            .map_err(|_| anyhow::anyhow!("OUT_DIR not set. Are you running this from build.rs?"))?;
+        let dest = Path::new(&out_dir).join(format!("{}.spv", label));
+        compiled.output(&dest).map_err(Into::into)
+    }
+
+    pub fn build(self, target: SlangTarget) -> anyhow::Result<CompiledSlangShader> {
+        let slang_dir = PathBuf::from(env!("SLANG_DIR"));
+        let slangc_path = slang_dir.join("bin").join(if cfg!(windows) { "slangc.exe" } else { "slangc" });
+
+        if !slangc_path.exists() {
+            anyhow::bail!("slangc executable not found at {}", slangc_path.display());
+        }
+
+        let mut cmd = std::process::Command::new(slangc_path);
+        let mut args_record = String::from("slangc");
+
+        let mut temp_files = Vec::new();
+
+        cmd.arg("-target").arg(target.as_arg());
+
+        use std::fmt::Write;
+        write!(&mut args_record, " -target {}", target.as_arg())?;
+
+        for (source_idx, source) in self.sources.iter().enumerate() {
+            let path_to_use = if let Some(path) = &source.path {
+                path.clone()
+            } else {
+                let mut temp = tempfile::Builder::new()
+                    .suffix(".slang")
+                    .tempfile()?;
+
+                use std::io::Write as IoWrite;
+                temp.write_all(source.content.as_bytes())?;
+
+                let p = temp.path().to_path_buf();
+                temp_files.push(temp);
+                p
+            };
+
+            cmd.arg(&path_to_use);
+            write!(&mut args_record, " {}", path_to_use.display())?;
+
+            for entry in &self.entries {
+                if entry.source_index == Some(source_idx) {
+                    cmd.arg("-entry").arg(&entry.name);
+                    write!(&mut args_record, " -entry {}", entry.name)?;
+
+                    if let Some(stage) = entry.stage {
+                        cmd.arg("-stage").arg(stage.as_arg());
+                        write!(&mut args_record, " -stage {}", stage.as_arg())?;
+                    }
+                }
+            }
+        }
+
+        cmd.args(&self.additional_args);
+        write!(&mut args_record, " {}", self.additional_args.join(" "))?;
+
+        let temp_dir = tempfile::tempdir()?;
+        let output_path = temp_dir.path().join("output");
+
+        cmd.arg("-o").arg(&output_path);
+        write!(&mut args_record, " -o {}", output_path.display())?;
+        println!("cmd: {:?}", cmd.get_args());
+        let output = cmd.output()?;
+
+        if !output.status.success() {
+            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
+            return Err(anyhow::anyhow!("Compilation error: {}", stderr));
+        }
+
+
+        let binary_output = std::fs::read(&output_path)?;
+
+        Ok(CompiledSlangShader::new(self.label, args_record, binary_output))
+    }
+}
+
+impl Default for SlangShaderBuilder {
+    fn default() -> Self {
+        Self::new("no named shader")
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ShaderStage {
+    Vertex,
+    Fragment,
+    Compute,
+    // wgpu doesnt support anything below this
+    Geometry,
+    Hull,
+    Domain,
+    RayGen,
+    Intersection,
+    AnyHit,
+    ClosestHit,
+    Miss,
+    Callable,
+}
+
+impl ShaderStage {
+    /// Convert to the slangc command-line argument
+    pub fn as_arg(&self) -> &'static str {
+        match self {
+            Self::Vertex => "vertex",
+            Self::Fragment => "fragment",
+            Self::Compute => "compute",
+            Self::Geometry => "geometry",
+            Self::Hull => "hull",
+            Self::Domain => "domain",
+            Self::RayGen => "raygeneration",
+            Self::Intersection => "intersection",
+            Self::AnyHit => "anyhit",
+            Self::ClosestHit => "closesthit",
+            Self::Miss => "miss",
+            Self::Callable => "callable",
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum SlangTarget {
+    Unknown,
+    None,
+
+    // HLSL/DirectX
+    Hlsl,
+    Dxbc,
+    DxbcAssembly,
+    Dxil,
+    DxilAssembly,
+
+    // GLSL/Vulkan/SPIR-V
+    Glsl,
+    /// Most optimal for WGPU and Vulkan/ 
+    SpirV,
+    SpirVAssembly,
+
+    // C/C++
+    C,
+    Cpp,
+    CppHeader,
+    TorchBinding,
+    HostCpp,
+
+    // Executables and Libraries
+    Executable,
+    ShaderSharedLibrary,
+    SharedLibrary,
+
+    // CUDA
+    Cuda,
+    CudaHeader,
+    Ptx,
+    CuBin,
+
+    // Host Callable
+    HostCallable,
+    ShaderObjectCode,
+    HostHostCallable,
+
+    // Metal
+    Metal,
+    MetalLib,
+    MetalLibAssembly,
+
+    // WebGPU
+    Wgsl,
+    WgslSpirVAssembly,
+    WgslSpirV,
+
+    // Slang VM
+    SlangVm,
+
+    // LLVM
+    HostObjectCode,
+    LlvmHostIr,
+    LlvmShaderIr,
+}
+
+impl SlangTarget {
+    /// Convert to the slangc command-line argument
+    pub fn as_arg(&self) -> &'static str {
+        match self {
+            Self::Unknown => "unknown",
+            Self::None => "none",
+            Self::Hlsl => "hlsl",
+            Self::Dxbc => "dxbc",
+            Self::DxbcAssembly => "dxbc-asm",
+            Self::Dxil => "dxil",
+            Self::DxilAssembly => "dxil-asm",
+            Self::Glsl => "glsl",
+            Self::SpirV => "spirv",
+            Self::SpirVAssembly => "spirv-asm",
+            Self::C => "c",
+            Self::Cpp => "cpp",
+            Self::CppHeader => "hpp",
+            Self::TorchBinding => "torch-binding",
+            Self::HostCpp => "host-cpp",
+            Self::Executable => "exe",
+            Self::ShaderSharedLibrary => "shader-dll",
+            Self::SharedLibrary => "dll",
+            Self::Cuda => "cuda",
+            Self::CudaHeader => "cuh",
+            Self::Ptx => "ptx",
+            Self::CuBin => "cubin",
+            Self::HostCallable => "host-callable",
+            Self::ShaderObjectCode => "shader-object-code",
+            Self::HostHostCallable => "host-host-callable",
+            Self::Metal => "metal",
+            Self::MetalLib => "metallib",
+            Self::MetalLibAssembly => "metallib-asm",
+            Self::Wgsl => "wgsl",
+            Self::WgslSpirVAssembly => "wgsl-spirv-asm",
+            Self::WgslSpirV => "wgsl-spirv",
+            Self::SlangVm => "slang-vm",
+            Self::HostObjectCode => "host-object-code",
+            Self::LlvmHostIr => "llvm-ir",
+            Self::LlvmShaderIr => "llvm-shader-ir",
+        }
+    }
+
+    /// Parse from a string (supports all aliases)
+    pub fn from_str(s: &str) -> Option<Self> {
+        match s {
+            "unknown" => Some(Self::Unknown),
+            "none" => Some(Self::None),
+            "hlsl" => Some(Self::Hlsl),
+            "dxbc" => Some(Self::Dxbc),
+            "dxbc-asm" | "dxbc-assembly" => Some(Self::DxbcAssembly),
+            "dxil" => Some(Self::Dxil),
+            "dxil-asm" | "dxil-assembly" => Some(Self::DxilAssembly),
+            "glsl" => Some(Self::Glsl),
+            "spirv" => Some(Self::SpirV),
+            "spirv-asm" | "spirv-assembly" => Some(Self::SpirVAssembly),
+            "c" => Some(Self::C),
+            "cpp" | "c++" | "cxx" => Some(Self::Cpp),
+            "hpp" => Some(Self::CppHeader),
+            "torch" | "torch-binding" | "torch-cpp" | "torch-cpp-binding" => Some(Self::TorchBinding),
+            "host-cpp" | "host-c++" | "host-cxx" => Some(Self::HostCpp),
+            "exe" | "executable" => Some(Self::Executable),
+            "shader-sharedlib" | "shader-sharedlibrary" | "shader-dll" => Some(Self::ShaderSharedLibrary),
+            "sharedlib" | "sharedlibrary" | "dll" => Some(Self::SharedLibrary),
+            "cuda" | "cu" => Some(Self::Cuda),
+            "cuh" => Some(Self::CudaHeader),
+            "ptx" => Some(Self::Ptx),
+            "cuobj" | "cubin" => Some(Self::CuBin),
+            "host-callable" | "callable" => Some(Self::HostCallable),
+            "object-code" | "shader-object-code" => Some(Self::ShaderObjectCode),
+            "host-host-callable" => Some(Self::HostHostCallable),
+            "metal" => Some(Self::Metal),
+            "metallib" => Some(Self::MetalLib),
+            "metallib-asm" => Some(Self::MetalLibAssembly),
+            "wgsl" => Some(Self::Wgsl),
+            "wgsl-spirv-asm" | "wgsl-spirv-assembly" => Some(Self::WgslSpirVAssembly),
+            "wgsl-spirv" => Some(Self::WgslSpirV),
+            "slangvm" | "slang-vm" => Some(Self::SlangVm),
+            "host-object-code" => Some(Self::HostObjectCode),
+            "llvm-host-ir" | "llvm-ir" => Some(Self::LlvmHostIr),
+            "llvm-shader-ir" => Some(Self::LlvmShaderIr),
+            _ => None,
+        }
+    }
+
+    /// Check if this target produces binary output
+    pub fn is_binary(&self) -> bool {
+        matches!(
+            self,
+            Self::Dxbc | Self::Dxil | Self::SpirV | Self::Executable
+            | Self::ShaderSharedLibrary | Self::SharedLibrary | Self::CuBin
+            | Self::MetalLib | Self::WgslSpirV | Self::SlangVm
+            | Self::HostObjectCode | Self::ShaderObjectCode
+        )
+    }
+
+    /// Check if this target is suitable for wgpu
+    pub fn is_wgpu_compatible(&self) -> bool {
+        matches!(self, Self::SpirV | Self::Wgsl)
+    }
+}
+
+impl std::fmt::Display for SlangTarget {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.as_arg())
+    }
+}
+
+impl std::str::FromStr for SlangTarget {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::from_str(s).ok_or_else(|| format!("Unknown Slang target: {}", s))
+    }
+}
+
+pub enum Profile {
+    Sm(String),
+    Glsl(String),
+    Vs(String),
+    Hs(String),
+    Ds(String),
+    Gs(String),
+    Ps(String),
+}
+
+impl Display for Profile {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let string = match self {
+            Profile::Sm(v) => format!("sm_{v}"),
+            Profile::Glsl(v) => format!("glsl_{v}"),
+            Profile::Vs(v) => format!("vs_{v}"),
+            Profile::Hs(v) => format!("hs_{v}"),
+            Profile::Ds(v) => format!("ds_{v}"),
+            Profile::Gs(v) => format!("gs_{v}"),
+            Profile::Ps(v) => format!("ps_{v}"),
+        };
+        write!(f, "{}", string)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_compile() {
+        let result = SlangShaderBuilder::new("light")
+            .add_source_str(include_str!("../test/light.slang"))
+            .entry_with_stage("vs_main", ShaderStage::Vertex)
+            .entry_with_stage("fs_main", ShaderStage::Fragment)
+            .build(SlangTarget::SpirV);
+        assert!(result.is_ok());
+    }
+
+    #[test]
+    fn test_from_bytes() {
+        let bytes = vec![1, 2, 3, 4];
+        let shader = CompiledSlangShader::from_bytes("idk", &bytes);
+        assert_eq!(shader.source, bytes);
+        assert_eq!(shader.label(), "unknown");
+    }
+}
\ No newline at end of file
diff --git a/crates/slank/src/utils.rs b/crates/slank/src/utils.rs
new file mode 100644
index 0000000..0461cee
--- /dev/null
+++ b/crates/slank/src/utils.rs
@@ -0,0 +1,16 @@
+/// WGPU based traits that are useful for those using the [wgpu] crate.
+#[cfg(feature = "use-wgpu")]
+pub trait WgpuUtils {
+    /// Creates a new [`wgpu::ShaderModuleDescriptor`] for you to create your own shaders with.
+    fn create_wgpu_shader(&self) -> wgpu::ShaderModuleDescriptor<'_>;
+}
+
+#[cfg(feature = "use-wgpu")]
+impl WgpuUtils for crate::CompiledSlangShader {
+    fn create_wgpu_shader(&self) -> wgpu::ShaderModuleDescriptor<'_> {
+        wgpu::ShaderModuleDescriptor {
+            label: Some(self.label.as_str()),
+            source: wgpu::util::make_spirv(self.source.as_ref()),
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/slank/test/light.slang b/crates/slank/test/light.slang
new file mode 100644
index 0000000..af1dd42
--- /dev/null
+++ b/crates/slank/test/light.slang
@@ -0,0 +1,56 @@
+struct Light {
+    float4 position;
+    float4 direction; // x, y, z, outer_cutoff_angle
+    float4 color; // r, g, b, light_type (0, 1, 2)
+
+    float constant;
+    float lin;
+    float quadratic;
+
+    float cutoff;
+};
+
+struct CameraUniform {
+    float4 view_pos;
+    float4x4 view_proj;
+};
+
+[[vk::binding(0, 0)]]
+ConstantBuffer<CameraUniform> camera;
+
+[[vk::binding(1, 0)]]
+ConstantBuffer<Light> light;
+
+struct VertexInput {
+    float3 position : POSITION;
+    float4 model_matrix_0 : TEXCOORD5;
+    float4 model_matrix_1 : TEXCOORD6;
+    float4 model_matrix_2 : TEXCOORD7;
+    float4 model_matrix_3 : TEXCOORD8;
+};
+
+struct VertexOutput {
+    float4 clip_position : SV_Position;
+    float3 color : COLOR0;
+};
+
+
+[shader("vertex")]
+VertexOutput vs_main(VertexInput input) {
+    float4x4 model_matrix = float4x4(
+        input.model_matrix_0,
+        input.model_matrix_1,
+        input.model_matrix_2,
+        input.model_matrix_3
+    );
+
+    VertexOutput output;
+    output.clip_position = mul(camera.view_proj, mul(model_matrix, float4(input.position, 1.0)));
+    output.color = light.color.xyz;
+    return output;
+}
+
+[shader("fragment")]
+float4 fs_main(VertexOutput input) : SV_Target0 {
+    return float4(input.color, 1.0);
+}
\ No newline at end of file