# shaderc-rs-py
:toc:

`shaderc-rs-py` provides Python bindings for the `shaderc` library through the `shaderc-rs` Rust bindings for compiling GLSL shaders to SPIR-V binary or assembly directly from Python code. By leveraging `PyO3`, `shaderc-rs-py` aims to provide a flexible API surface, including support for advanced shader compilation options, preprocessing capabilities, macro definitions, and SPIR-V assembly generation.

image::assets/images/shaderc-rs-py-20250124.webp[shaderc-rs-py Example Explorer]

[WARNING]
====
This library is currently experimental. While it offers a broad range of functionality, it may contain memory leaks or other issues. It has not been thoroughly tested, and it is recommended that you use it cautiously in production contexts.
====

## Features

* Provides Python bindings over shaderc-rs (Rust) for GLSL-to-SPIR-V compilation.  
* Offers fine-grained control of compilation options (optimization levels, target environment, debug info, macro definitions, and more).  
* Generates both SPIR-V binary and assembly.  
* Includes examples demonstrating real-time shader editing with wgpu-py and PySide6.  
* Can be built into Python wheels for multiple platforms (Windows, macOS, and Linux) using Maturin and PyO3.  

## Installation

The recommended Python version is 3.12 or higher. You can clone the repository and build from source, or install from the PyPI package release (`pip install shaderc-rs-py` or `uv add shaderc-rs-py`).

If you plan to work on this project locally, follow these steps:

1. Install UV (a CLI tool for Python environment management).  
+
[source,bash]
----
curl -LsSf https://astral.sh/uv/install.sh | sh
# or on Windows PowerShell:
# powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
----

2. Update UV and synchronize the workspace:  
+
[source,bash]
----
uv self update
uv sync
----

3. Activate the virtual environment and install any development dependencies. For instance:  
+
[source,bash]
----
source .venv/bin/activate
# or on Windows PowerShell:
# .venv\Scripts\activate
uv add ruff
maturin develop --uv
----

Alternatively, to set up the project directly:

[source,bash]
----
git clone https://github.com/mehmetoguzderin/shaderc-rs-py
cd shaderc-rs-py
uv sync
code .
----

## Usage

### Basic Examples

#### Basic Vertex Shader Compilation

[source,python]
----
import shadercrs

compiler = shadercrs.Compiler()
options = shadercrs.CompileOptions()

vertex_shader = """
#version 450
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 texcoord;

layout(location = 0) out vec2 fragTexcoord;

void main() {
    gl_Position = vec4(position, 1.0);
    fragTexcoord = texcoord;
}
"""

result = compiler.compile_into_spirv(
    source=vertex_shader,
    stage="vertex",
    input_file_name="shader.vert",
    entry_point="main",
)

spirv_binary = result.as_binary()
print(f"Compiled SPIR-V size: {len(spirv_binary)} words")

if result.get_num_warnings() > 0:
    print("Compilation warnings:", result.get_warning_messages())
----

### Advanced Examples

#### Fragment Shader with Macros and Assembly

[source,python]
----
import shadercrs

compiler = shadercrs.Compiler()
options = shadercrs.CompileOptions()

options.set_source_language("glsl")
options.set_target_env("vulkan", "vulkan_1_1")
options.set_optimization_level("performance")
options.set_generate_debug_info()
options.add_macro_definition("MAX_VERTICES", "1024")

fragment_shader = """
#version 450
#define MAX_LIGHTS 4

layout(location = 0) in vec2 fragTexcoord;
layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(fragTexcoord, 0.0, 1.0);
    #ifdef MAX_VERTICES
    #endif
}
"""

result = compiler.compile_into_spirv(
    source=fragment_shader,
    stage="fragment",
    input_file_name="shader.frag",
    entry_point="main",
    options=options,
)

spirv_asm = compiler.compile_into_spirv_assembly(
    source=fragment_shader,
    stage="fragment",
    input_file_name="shader.frag",
    entry_point="main",
    options=options,
)

print("SPIR-V Assembly:", spirv_asm.as_text())
----

#### Preprocessing with Macro Definitions

[source,python]
----
import shadercrs

def compile_shader(source, shader_value):
    stage = "vertex"
    compiler = shadercrs.Compiler()
    options = shadercrs.CompileOptions()

    options.add_macro_definition("SHADER_VALUE", shader_value)
    options.add_macro_definition("DEBUG", "1")

    try:
        preprocessed = compiler.preprocess(
            source=source,
            input_file_name=f"shader.{stage}",
            entry_point="main",
            options=options,
        )

        print("Preprocessed source:")
        print(preprocessed.as_text())

        result = compiler.compile_into_spirv(
            source=source,
            stage=stage,
            input_file_name=f"shader.{stage}",
            entry_point="main",
            options=options,
        )
        return result.as_binary()

    except Exception as e:
        print(f"Compilation failed: {e}")
        return None

shader_with_value = """
#version 450

void main() {
    float value = SHADER_VALUE;
}
"""

spirv = compile_shader(shader_with_value, "1.0")

if spirv is not None:
    print("Compilation successful")
----

#### Compute Shader Compilation to SPIR-V File

[source,python]
----
import shadercrs

compiler = shadercrs.Compiler()
options = shadercrs.CompileOptions()

compute_shader = """
#version 450

layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

layout(set = 0, binding = 0) buffer Data {
    float values[];
} data;

void main() {
    uint index = gl_GlobalInvocationID.x;
    data.values[index] *= 2.0;
}
"""

try:
    result = compiler.compile_into_spirv(
        source=compute_shader,
        stage="compute",
        input_file_name="compute.comp",
        entry_point="main",
        options=options,
    )
    spirv_binary = result.as_binary()
    spirv_bytes = result.as_binary_u8()

    with open("compute.spv", "wb") as f:
        f.write(bytes(spirv_bytes))

    print(f"Compute shader compiled successfully: {len(spirv_binary)} words")

except Exception as e:
    print(f"Compilation failed: {e}")
----

#### Querying SPIR-V Version and Parsing Profile

[source,python]
----
import shadercrs

def main():
    version, revision = shadercrs.get_spirv_version_py()
    print("SPIR-V version:", version, "revision:", revision)

    compiler = shadercrs.Compiler()
    opts = shadercrs.CompileOptions()
    opts.set_source_language("glsl")
    opts.set_target_env("vulkan", "vulkan_1_2")
    opts.set_optimization_level("performance")
    opts.set_generate_debug_info()

    glsl_source = """
    #version 450
    layout(location = 0) in vec3 inPos;
    void main() {
        gl_Position = vec4(inPos, 1.0);
    }
    """

    artifact = compiler.compile_into_spirv(
        glsl_source, "vertex", "my_shader.glsl", "main", opts
    )

    if artifact.is_empty():
        print("Compilation produced empty result!")
    else:
        print("Compilation warnings:", artifact.get_num_warnings())
        if artifact.get_num_warnings() != 0:
            print("Warning messages:")
            print(artifact.get_warning_messages())

        spirv_bytes = artifact.as_binary()
        print("SPIR-V word count:", len(spirv_bytes))

    asm_artifact = compiler.compile_into_spirv_assembly(glsl_source, "vertex")
    asm_text = asm_artifact.as_text()
    print("Assembly text:\n", asm_text)

    maybe_profile = shadercrs.parse_version_profile_py("450core")
    print("Parsed 450core =>", maybe_profile)

if __name__ == "__main__":
    main()
----

#### Basic Compute Shader Compilation as `uv` Script

[source,python]
----
#!/usr/bin/env -S uv run
# /// script
# dependencies = [
#   "shaderc-rs-py",
# ]
# [tool.uv]
# exclude-newer = "2025-02-20T00:00:00Z"
# ///

import shadercrs

compiler = shadercrs.Compiler()
options = shadercrs.CompileOptions()

compute_shader = """
#version 450
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

void main() {
}
"""

result = compiler.compile_into_spirv(
    source=compute_shader,
    stage="compute",
    input_file_name="shader.comp",
    entry_point="main",
)

spirv_binary = result.as_binary()
print(f"Compiled SPIR-V size: {len(spirv_binary)} words")

if result.get_num_warnings() > 0:
    print("Compilation warnings:", result.get_warning_messages())
----

## Development

To develop `shaderc-rs-py`, you can use VS Code in tandem with UV to manage your environment. Run the following commands in your project directory:

[source,bash]
----
uv self update
uv sync
source .venv/bin/activate
uv add ruff
maturin develop --uv
----

Then open the project in VS Code (or any other editor) and begin making changes. You can run tests (once tests are implemented) by installing `pytest` inside your environment and executing:

[source,bash]
----
pytest
----

## Running the Example Explorer

A real-time shader editing example is located in the `examples` directory. This example uses PySide6 for the GUI and wgpu-py for GPU rendering. It displays a constantly rendered scene in which you can modify a fragment shader and see changes immediately.

Steps to run the example:

1. Ensure you have wgpu-py and PySide6 installed (already part of the requirements in the `pyproject.toml` file).
+
[source,bash]
----
uv sync
----

2. Activate your environment and navigate to the `examples` directory.  
3. Run the Python script.
+
[source,bash]
----
python explorer.py
----

Once launched, a window will appear where you can adjust the optimization level, edit the shader source, and observe the effects in real-time.
