Metadata-Version: 2.2
Name: bazalt
Version: 0.15.0
Summary: Python library for rapid GPU shader prototyping using Vulkan
Author: Piotr Szamocki
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Project-URL: Homepage, https://github.com/SzamockiP/bazalt
Project-URL: Repository, https://github.com/SzamockiP/bazalt
Requires-Python: >=3.10
Provides-Extra: examples
Requires-Dist: PyGLM; extra == "examples"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: numpy; extra == "test"
Description-Content-Type: text/markdown

# Bazalt

**Bazalt** is a Python library for the Vulkan API. Use it to prototype GPU and shader
work, and to build graphical applications. A C++23 core does the Vulkan work. The Python
layer keeps the setup code short.

## Installation

Install `bazalt` with `pip`:

```bash
pip install bazalt
```

Bazalt supplies prebuilt wheels for Windows and Linux. To build from source, you need a
C++23 compiler (GCC 14+ or MSVC 19.36+, Visual Studio 17.6) and the Vulkan SDK.

## Key Features

- **Vulkan Core:** Bazalt calls Vulkan for all GPU work. You keep explicit control of the hardware.
- **Shader Compilation:** `ctx.compile_shader()` compiles GLSL vertex, fragment and compute shaders from your code.
- **Compute Pipelines:** `ctx.compute_pipeline()` and `cmd.dispatch()` run GPU compute. The results go back into NumPy. You need no images.
- **Storage Images:** Compute shaders also write images. `.storage_image(binding)` and `set_storage_image()` bind a read/write image for `imageStore` and `imageLoad`. Use this for post-processing and procedural textures. Bazalt records the barriers and the layout transitions between the dispatch and a later sample.
- **Cubemaps and Texture Arrays:** Pass a list of layers to `ctx.create_image([...], cube=True)` or to `ctx.load_image([...6 paths...], cube=True)` for a cubemap. Omit `cube=` for a texture array. One mechanism gives you `samplerCube` and `sampler2DArray`. Compute can fill empty layered images, for example a procedural skybox. See `examples/14_skybox`.
- **Mipmaps:** `load_image()` makes mips for files by default. Pass `mipmaps=False` to stop this. `create_image(array, mipmaps=True)` makes mips for a NumPy texture. `create_image(w, h, mip_levels=N)` allocates an empty chain, and `cmd.generate_mipmaps(image)` fills it from mip 0. This works across every array layer and every cube face.
- **Automatic Barriers:** Bazalt computes the barriers for hazards between resources at record time. Examples: dispatch to dispatch, a compute-written buffer to a vertex fetch, a compute-written image to a sample. `Context(auto_barriers=False)` gives you manual control through `cmd.barrier()`.
- **Pipeline and Buffer Management:** A builder makes graphics pipelines. One function makes every buffer type.
- **Command Buffers:** Command recording is explicit. Calls chain, as in `cmd.bind_pipeline(p).draw(3)`. The block `with cmd.rendering(target):` ends the pass for you.
- **Asynchronous Texture Streaming:** `ctx.load_image()` returns immediately. The decode and the GPU copy run in the background. Anything that samples the image waits for it. `ctx.upload_progress` gives you the data for a loading bar.
- **Hot Reload:** `Context(hot_reload=True)` watches the shaders you loaded, their `#include` files, and your images. Bazalt applies edits live: shaders recompile and rebuild their pipelines in place, and images upload again into the same handle. Bazalt logs a typo or a bad file and keeps the last good version on screen. A mistake does not stop the application. See `examples/12_hot_reload`.
- **Frame Timing and Debug Names:** `Context(gpu_timing=True)` makes `renderer.gpu_time_ms` report the GPU time of a recent frame. This is opt-in and costs nothing when off. Use `t = cmd.timer()` and `t.stop()` to time a slice of a recording, then read `t.ms`. A timer is a handle, so you need no `with` block, and it reads back headless. The `name=` argument and the `.name()` method label buffers, images and pipelines, so validation messages name the object.
- **Headless Rendering:** Draw into an offscreen `RenderTarget` and read the pixels back as a NumPy array. You need no window and no display.
- **Render-to-Texture, MRT and Shadow Maps:** Target attachments are ordinary `Image` objects in any supported `Format`. Sample `target.color[0]`, or `target.depth` on a depth-only target, like any other texture.
- **Multi-Window:** One Context drives any number of windows. `ctx.begin_frame()` opens the frame. Each window calls `renderer.acquire()` and `renderer.present(cmd)`, and owns its own swapchain. If you resize or close one window, the other windows continue to render. See `examples/19_multi_window`.
- **GPU Selection:** `bz.list_devices()` lists every card with its name, type, VRAM, API version and per-feature support, before a Context exists. `Context(device=...)` runs on the card you chose. Omit the argument and bazalt chooses for you.
- **Multi-Context and Multi-GPU:** Any number of Contexts can be alive at the same time, on one card or on different cards. You can bake with compute on one GPU and render on the other. `other_ctx.create_image(image)` moves the pixels across with their format, their layers and their cube property. Every other resource stays with its Context. If you give a resource to the wrong command buffer, you get a `ResourceError` that names the mistake, not a driver crash. See `examples/20_multi_context`.
- **Wide Hardware Support:** Bazalt needs Vulkan 1.2 and uses 1.3 where the driver has it, so it also runs on older integrated GPUs. You request capabilities by name, never by version or by extension.
- **Separated Layers:** Windowing (GLFW), the Vulkan Context (GPU initialization) and render targets stay separate. A window is one render target among others.

## Quick Start: Rendering Without a Window

The code below runs with no display attached. You can use it from CI and from tests:

```python
import bazalt as bz

ctx = bz.Context()
target = bz.RenderTarget(ctx, 800, 600, depth=bz.Format.D32F)

pipeline = (ctx.graphics_pipeline()
    .vertex_shader(ctx.compile_shader("triangle.vert", bz.ShaderStage.VERTEX))
    .fragment_shader(ctx.compile_shader("triangle.frag", bz.ShaderStage.FRAGMENT))
    .vertex_format([bz.VertexFormat.FLOAT3, bz.VertexFormat.FLOAT3])
    .build(target))

# Interleaved position (x,y,z) and color (r,g,b)
vbuf = ctx.create_buffer([
     0.0, -0.5, 0.0,   1.0, 0.0, 0.0,
    -0.5,  0.5, 0.0,   0.0, 1.0, 0.0,
     0.5,  0.5, 0.0,   0.0, 0.0, 1.0,
], bz.BufferType.VERTEX, bz.MemoryUsage.STATIC, bz.DataType.FLOAT)

cmd = ctx.create_command_buffer()
cmd.begin()
with cmd.rendering(target, clear_color=[0.1, 0.2, 0.3, 1.0]) as c:
    c.bind_pipeline(pipeline).bind_vertex_buffer(vbuf).draw(3)

ctx.submit(cmd)

pixels = target.read_pixels()   # numpy (600, 800, 4) uint8
```

## Quick Start: GPU Compute

Compute needs no window and no images. Dispatch, then read the storage buffer back as a
NumPy array:

```python
import numpy as np
import bazalt as bz

ctx = bz.Context()

# double.comp: values[i] *= 2.0 over a std430 float array, local_size_x = 64
sim = (ctx.compute_pipeline()
    .shader(ctx.compile_shader("double.comp", bz.ShaderStage.COMPUTE))
    .storage_buffer(0)      # no stage argument — compute has exactly one stage
    .build())               # no target — compute has no attachments

data = np.arange(128, dtype=np.float32)
sbuf = ctx.create_buffer(data, bz.BufferType.STORAGE, bz.MemoryUsage.STATIC)

pool = ctx.create_descriptor_pool(max_sets=4, storage_buffers=4)
dset = pool.allocate_set(sim, set=0)
dset.set_buffer(0, sbuf)

cmd = ctx.create_command_buffer()
cmd.begin()
cmd.bind_pipeline(sim).bind_descriptor_set(dset, sim, set=0).dispatch(128 // 64)
ctx.submit(cmd)

assert np.allclose(sbuf.read(np.float32), data * 2)
```

You can mix compute and rendering in one command buffer. A dispatch writes the vertices
and a draw reads them, with no extra code. Bazalt records the barrier between them. See
`examples/11_particles`.

## Quick Start: Hot Reload

Add one keyword argument. Bazalt then watches the files it loaded: the shaders, their
`#include` files, and the images. On each save, bazalt recompiles and uploads again:

```python
ctx = bz.Context(logger, hot_reload=True)

vert = ctx.compile_shader("shader.vert", bz.ShaderStage.VERTEX)
frag = ctx.compile_shader("shader.frag", bz.ShaderStage.FRAGMENT)
tex  = ctx.load_image("wall.png")
pipeline = ctx.graphics_pipeline().vertex_shader(vert).fragment_shader(frag)...build(renderer)

while window.is_open():
    bz.poll_events()
    ctx.begin_frame()                  # edits are applied here (and at ctx.submit)
    if renderer.acquire():
        renderer.present(cmd)          # renderer.gpu_time_ms (with gpu_timing=True) gives GPU frame timing
```

When you edit `shader.frag`, bazalt rebuilds the pipeline in place. When you save
`wall.png` again with the same size and the same format, bazalt uploads it into the same
handle, so the descriptor sets stay correct. A shader typo logs a `ShaderError`, and the
last good pipeline continues to render. A mistake does not stop the application. For the
full demo, see `examples/12_hot_reload`.

## Quick Start: Drawing a Triangle

This example starts the window, the Vulkan Context and the SwapchainRenderer. It then
compiles the shaders, creates a pipeline and draws a triangle with three colors.

```python
import bazalt as bz

# 1. Initialize Logger and register a callback.
#    Each message carries its severity and source as data, so you can filter
#    without parsing strings. This is optional: a Context created without one
#    reports warnings on stderr by default.
logger = bz.Logger(min_severity=bz.Severity.WARNING)
@logger.on_message
def on_message(msg):
    print(f"[{msg.severity}] {msg.text}")

# 2. Create the window, Vulkan Context, and SwapchainRenderer
window = bz.Window(1024, 720, "Bazalt Demo - Triangle", logger=logger)
ctx = bz.Context(logger)
renderer = bz.SwapchainRenderer(window, ctx)

if __name__ == "__main__":
    # Load and compile shaders. The vertex shader processes our geometry,
    # and the fragment shader determines the final color of the pixels.
    # Shaders are compiled through the Context.
    vert_spv = ctx.compile_shader("triangle.vert", bz.ShaderStage.VERTEX)
    frag_spv = ctx.compile_shader("triangle.frag", bz.ShaderStage.FRAGMENT)

    # The pipeline is a baked state object that tells the GPU how to interpret our data.
    # It is built against a render target, which supplies the color/depth formats.
    # A SwapchainRenderer is one, so is an offscreen RenderTarget — same call.
    pipeline = (ctx.graphics_pipeline()
        .vertex_shader(vert_spv)
        .fragment_shader(frag_spv)
        .vertex_format([bz.VertexFormat.FLOAT3, bz.VertexFormat.FLOAT3]) # Position + Color
        .build(renderer))

    # We interleave Position (x,y,z) and Color (r,g,b) in a single flat array.
    # Vulkan's Normalized Device Coordinates (NDC) range from -1 to 1,
    # where Y points downwards and X points to the right.
    vertices = [
         0.0, -0.5, 0.0,   1.0, 0.0, 0.0, # Top / Red
        -0.5,  0.5, 0.0,   0.0, 1.0, 0.0, # Bottom-Left / Green
         0.5,  0.5, 0.0,   0.0, 0.0, 1.0, # Bottom-Right / Blue
    ]
    
    # Create Vertex Buffer through the Context
    vbuf = ctx.create_buffer(vertices, bz.BufferType.VERTEX, bz.MemoryUsage.STATIC, bz.DataType.FLOAT)
    
    # Create Index Buffer through the Context
    ibuf = ctx.create_buffer([0, 1, 2], bz.BufferType.INDEX, bz.MemoryUsage.STATIC, bz.DataType.UINT32)

    # Command buffers store a sequence of commands for the GPU.
    # For a static triangle, we can record this buffer once during initialization 
    # and submit the same pre-recorded buffer every frame to save CPU time.
    cmd = ctx.create_command_buffer()

    cmd.begin()

    # begin_rendering names the target you are drawing into, clears it, and sets
    # a viewport and scissor covering it. Naming the target is what lets the same
    # code render to a window or to an offscreen image.
    cmd.begin_rendering(renderer, clear_color=[0.1, 0.2, 0.3, 1.0])

    # Bind the baked pipeline and the geometry buffers
    cmd.bind_pipeline(pipeline)
    cmd.bind_vertex_buffer(vbuf)
    cmd.bind_index_buffer(ibuf)

    # Draw 3 indices (1 triangle)
    cmd.draw_indexed(3)
    cmd.end_rendering(renderer)

    # Main game/rendering loop
    while window.is_open():
        bz.poll_events()
        
        # begin_frame opens one logical frame for every window on this
        # Context; acquire() takes this window's swapchain image and returns
        # False when it should sit the frame out (minimized, mid-resize)
        ctx.begin_frame()
        if renderer.acquire():
            renderer.present(cmd)
```
